Одно из самых полезных применений дженериков, которое у меня было — это написание врапперов, которые добавляют строгую типизацию в обработку чего-либо и уменьшают бойлерплейт. Например, обработка http-запросов. Обычно используется роутер и в него регистрируются хендлеры для разных путей, например:
router := httprouter.New()
router.POST("/api/products", productsHandler.Handle)
router.POST("/api/get_free_slots", getFreeTimeSlotsHandler.Handle)
router.POST("/api/create_visit", createVisitHandler.Handle)
При этом, функция Handle у каждого хендлера это обычно что-то типа
func(w http.ResponseWriter, r *http.Request)
Хендлер уже внутри себя сам читает тело, парсит его из json, потом формирует ответ, итд.
Хотелось добавить сюда типизацию и какую-то структуру, поэтому я написал
простой враппер
type Validatable interface {
Validate() error
}
func Wrap[Req Validatable, Res any](fn func(ctx context.Context, req Req) (Res, error)) httprouter.Handle {
handler := func(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
writeError := func(statusCode int, err error) {
writer.WriteHeader(statusCode)
_, _ = writer.Write([]byte(err.Error()))
}
var req Req
bytes, err := io.ReadAll(request.Body)
if err != nil {
writeError(http.StatusBadRequest, err)
return
}
if err := json.Unmarshal(bytes, &req); err != nil {
writeError(http.StatusBadRequest, err)
return
}
if err := req.Validate(); err != nil {
writeError(http.StatusBadRequest, err)
return
}
resp, err := fn(request.Context(), req)
if err != nil {
writeError(http.StatusInternalServerError, err)
return
}
respBytes, err := json.Marshal(resp)
if err != nil {
writeError(http.StatusInternalServerError, err)
return
}
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write(respBytes)
}
return handler
}
Он берет на себя весь бойлерплейт по парсингу и валидации запроса, формированию ответа, работы с кодами, итд. Причем, через стандартный errors.Is иногда добавлял сюда еще возможность из хэндлера указать врапперу, какой http-код ответа отдавать. Обычно все функции враппера делал под конкретную задачу в конкретном проекте (можно, например, не только json тела парсить, но и урл и хедеры, итд).
Использование враппера выглядит так.
Мы пишем хэндлер, используя конкретные типы
type Handler struct {
userService *usersvc.Service
}
type Request struct {
UserID int64 `json:"user_id"`
}
func (c Request) Validate() error {
if c.UserID == 0 {
return errors.New("empty user id")
}
return nil
}
type Response struct {
Name string `json:"name"`
Age int `json:"age"`
}
func New(userService *usersvc.Service) *Handler {
return &Handler{userService: userService}
}
func (h Handler) Handle(ctx context.Context, req *Request) (*Response, error) {
user, err := h.userService.UserByID(ctx, req.UserID)
if err != nil {
return nil, err
}
return &Response{
Name: user.Name,
Age: user.Age,
}, nil
}
Через dependency injection даем хэндлеру все источники данных, изолируем хэндлер в отдельном пакете, четко прописываем ему типы Request и Response, пишем валидацию для запроса. Открыв хэндлер, разработчик сразу видит его контракты, весь бойлерплейт во враппере, остается написать только логику.
В итоге использование такого хэндлера+враппер выглядит так:
getUserHandler := getuser.New(userSvc)
router := httprouter.New()
router.POST("/api/get_user", wrapper.Wrap(productsHandler.getUserHandler))
Причем, тут даже не видно, что Wrap() имеет дженерик-параметры, потому что гошка самостоятельно выводит эти параметры из типов Request и Response из хэндлера и проверяет, что у Request есть метод Validate.
Подобные дженерик-врапперы применял в нескольких разных задачах. Кроме обработки запросов еще была система для запуска фоновых джоб по обработке данных, нужно было положить джобы в один слайс, но при этом, чтобы контракты джоб были типизированы. Поэтому враппером приводил конкретные типы структур из джоб к более универсальному виду с типами в interface{}. Получилось, что и типы все на компиляции проверяются и могу положить все джобы в одну коллекцию.