Задать вопрос
uvelichitel
@uvelichitel
habrahabr.ru/users/uvelichitel

Кто нибудь применял или видел generics в серьезных Go проектах?

Столько ждали, три года обсуждали. Наконец подвезли. И неплохо сделано.
Но, как то, ландшафт и не поменялся. В стандартной библиотеке вроде не применяется.
Подскажите, где можно посмотреть применение `type parameters` в крупных\значимых кодовых базах. Или, может быть, поделитесь опытом использования на производстве.
  • Вопрос задан
  • 1105 просмотров
Подписаться 2 Простой 13 комментариев
Решения вопроса 1
Одно из самых полезных применений дженериков, которое у меня было — это написание врапперов, которые добавляют строгую типизацию в обработку чего-либо и уменьшают бойлерплейт. Например, обработка 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{}. Получилось, что и типы все на компиляции проверяются и могу положить все джобы в одну коллекцию.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
@calculator212
Или, может быть, поделитесь опытом использования на производстве.
Хз что подразуемевается под серьезным проектом. На практике их получается не так часто использовать. Например изнально была функция, которая на вход получала слайс и преобразовывала его в таски и помещала в пул , потом добавилось несколько других источников данных, с которыми нужно было тоже самое делать. В целом форматы данных отличались, но всё можно было преобразовать к общему формату. Выделил общие методы и использовал дженирики. В целом можно было бы обойтись и без них, но в моей ситуации это было довольно удобно, по сути можно было бы заменить интерфейсом.
Реальный плюс дженериков в том, что в ide (по крайней мере в goland) проще искать код методов и ты уже сразу знаешь какие типы они реализуют, что упрощает поддержку кода (тут субъективно).
С одной стороны некоторых возможностей дженериков не хватает, но с другой стороны радует, что код на го не будет похож на плюсовый код в бусте, и в основном дженерики используются там где это нужно.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы