golang

Запилил 100 проектов на Go и вот что скажу

  • четверг, 6 марта 2025 г. в 00:00:12
https://habr.com/ru/articles/887946/

Когда начинаешь новый проект, то надо какое то время потратить, чтобы подобрать подходящие библиотеки, подготовить структуру проекта, связать все воедино. Это занимает прилично времени. Целый день вполне может уйти. А экономить на этом шаге не стоит. Это ведь как фундамент. И вот, 5 лет назад я опубликовал каркас, который собрал для себя, чтобы делать с ним проекты на Go.

С тех пор ко мне обращались люди и с вопросами, и с советами. Я менял компоненты, смотрел что получалось. Благо проектов было предостаточно. Вот и давайте подведем некий итог, что в заложенной структуре хорошо, а что не очень.

DI

По теме DI ко мне чаще всего обращались с критикой, в том числе и коллеги. Поэтому с этого и начнем. В основном предлагали 2 идеи

  • Зачем нам в Go использовать DI.

  • Если и использовать, то давай уж типобезопасную, например Wire.

И мы пробовали разное, ну разве что без DI не пробовали, так как проекты у нас разные, где-то и с десяток мегабайт кода набегает. А без DI там увы.

С Wire получается довольно много возни. А когда начали использовать генерики, то стало совсем неудобно. Приходилось в обход DI изобретать констукции. И фишка Go на этом начинает упраздняться. Я про простоту и скорость разработки.

Поэтому DI мы оставили контейнерный. У него есть недостаток - с ним вручную типы надо преобразовывать. Но если посмотреть на каркас, то в целом понятно, что основная логика спрятана в сервисах. А экземпляры только в 2 местах извлекаются - в файлике route/api.go, там контроллеры создаются. И в файликах консольных команд command/*. На этом и всё. То есть проконтроллировать типы тут очень просто, так как получение экземпляров не расползается по коду.

Вот так экземпляр получаем.

userService := dic.Container.Get(dic.UserService).(service.UserServiceInterface)

То есть тут без изменений. И опробовав разное, мы пришли к тому что и было у нас изначально. Единственное, что я сделал, переключился на оригинальный пакет со своего форка, в котором добавлял возможность переопределять сервисы после их определения. Я это делал чтобы мокать сервисы в тестах. Но такие тесты поддерживать сложно очень. И мы пришли к тому что делаем только юнит тесты на самые важные алгоритмы. И выносим такой код отдельно, чтобы и не приходилось ничего мокать.

Получили можно сказать золотую середину. И тесты есть на самое критичное, и разработка не замедляется из-за сложности тестов, которые надо поддерживать.

Ответы в формате JSON/XML

Тут я ничего не менял, но я отмечу, что пробовал разные бибилиотеки. И в проектах, где нагрузка на апи, сотни тысяч запросов в минуту, когда еще и объемные ответы надо отдавать, я в итоге формирую json/csv строку без библиотек вручную через буфер. Иначе забивается и не освобождается память. Это может привести к уходу в своп. Не знаю, может, в свежих версиях Go это и решено, но что-то уже не хочется пробовать наступать на эти грабли вновь.

Базовый CRUD

А вот тут в итоге убрал слой репозиториев в новых проектах. И в зависимости от проекта бывает, что добавляю его все же. Сервисы часто становятся этакими прокси для репозиториев, то есть функция сервиса содержит одну строку - вызов репозитория. Мне это не особо нравится.

Изначально я видел плюс в отдельном слое с репозиториями, так как можно было замокать репозиторий (работу с данными) для тестов. Но как описал выше, такие тесты делать и поддерживать очень трудозатратно. В итоге репозитории я объединяю с сервисами, а в сервисах описываю и бизнес логику, и работу с данными.

Функции сервиса в итоге выглядят теперь примерно так.

func (s UserService) GetUsers(name string) ([]entity.User, error) { var items []*entity.User res := s.db. Where("name ilike ?||'%'", name). Order("name"). Find(&items) if res.Error != nil { return nil, res.Error } return items, nil }

И еще, в целом мне не нравилось что в контроллерах с гидраторами получается громоздко и не особо наглядно, код как-то не воспронимается цельно. Их я тоже перестал использовать.

Вот примерно так теперь делаю, это хоть и менее по фен шую, но проще и нагляднее

type UserListParameters struct { Name string json:"name" form:"name" }

func (c UserController) List(ctx gin.Context) { var params UserListParameters if err := ctx.ShouldBind(&params); err != nil { BadRequestJSON(ctx, err.Error()) return } list, err := c.service.GetUsers(
params.Name) if err != nil { ServerErrorJSON(ctx, "Something went wrong") return } SuccessJSON(ctx, list) }

Логирование

По логированию в Go пакетов чуть больше, чем 1, и мне предлагали посмотреть разные. В том числе более производительные. Я пробовал, но в итоге снова остановился на logrus. Просто удобный интерфейс. Это относительно быстрый пакет. 

А еще более высокая производительность для меня тут не важна, так как логов пишу мало (чтобы от них была польза), либо направляю в ELK, а это само по себе уже медленно, и поэтому туда много логов писать нельзя, чтобы это не стало узким местом. Либо бывает, что имею огромные логи ошибок, когда что-то сломалось. А когда что-то сломалось, то что уж, кашу маслом не испортишь, пусть себе пишутся, просто очень быстро, а не супер-супер быстро.

Миграции и сидеры

С gorm-goose часто случалась какая-то ерунда, у коллег часто не стыковались зависимости. В итоге просто стал использовать пакет steinbacher/goose, вроде бы так получше.

А еще добавил сидеры (нет, не яблочные, а те что в папке db/seeds), чтобы в миграции кодить только изменения схемы БД, а в сидерах только наполнение табличек данными. Сидеры обычно делаю так, чтобы их можно было запускать по много раз, и при каждом деплое запускаю их, чтобы не забыться и не прошляпить при деплое.

А остальное в каркасе осталось плюс-минус как и было. Я в основном только зависимости обновляю. Вот этот каркас.

PS

Насчет 100 проектов за 5 лет я не приувеличил, наверное и побольше. Проектами я тут называю гит-репозитории. Но отмечу, что я их делал не в одиночку. Какие то делал больше собственноручно, а в каких-то только участвовал в разработке. 

Так вот, мне как техдиру, который старается держать в голове максимум деталей своего программного хозяйства, очень удобно иметь эти проекты максимально похожими. Если у вас еще не так, то весьма советую, это правда удобно.

  /')   |,\__/|
 ( (   _|  o o|._  Мяу
--------)))---)))--------