Ниже приведены мои варианты решения типовых задач, которые у меня возникали при разработке на golang.
func main()
Пока я не нашел удобный способ тестировать main()
, мой вариант - сократить эту функцию до одной строки и убрать ее из тестов.
Если репозиторий содержит бинарник с именем APP
, его функция main()
размещается в отдельном файле cmd/APP/main.go
со следующим содержанием:
log
- structured logging - чтобы парсить без геморроя, для прода это json stream. т.е. если в пакете логгирования есть строка формата - это сразу убираем
- поддержка терминала. Да, в проде хорош json, но при отладке хотелось бы цветной читабельный логгинг в терминал
Пока не было log/slog, я смотрел на тему так:
- для случаев, отличных от примитивных, необходима возможность парсить логи приложения. Тут мой выбор - structured logging и вариант его реализации - zap
- вместе с этим, зашивать конкретную имплементацию журналирования мне представляется не оптимальным, поэтому в пакетах использую logr
- в тестах, если надо собрать логи и потом проанализировать, я использую genericr
Теперь ситуация изменилась
- не осталось причин не использовать глобально пакет log/slog. Все его настройки можно сделать на старте, а работа с контекстом стала даже удобнее
- Для логгирования контекста вместо
log.FromContext
используюslog.ErrorContext
- если понадобится зависимость уровня логгирования от пакета - это решится в slog.Handler
log usage
Ссылка на log не передается конструктору и не является глобальной переменной. Она передается в конкретном вызове или через контекст.
Почему: В строку журнала может понадобиться добавить переменную со значением из текущего запроса (например - traceId, или еще что-то уровнем выше текущего пакета)
config
Для конфигурации приложения наиболее характерный пример для меня - использование библиотеки, у которой есть свой конфиг, при обновлении которого я бы не хотел ничего менять в своем приложении. Я использую go-flags и выглядит это так:
|
|
pg
Однажды, в mqbridge, мне понадобилось работать с каналом (db.Listen(channel).Channel()
), для этого я выбрал go-pg. В остальных случаях использую pgx
embedding
Выбор пакета для меня определяется ответом на вопрос - “Нужна ли UnionFS” (т.е. возможность локальным файлом заменить какой-то файл из дистрибутива)
Начиная с go 1.16, первичный вариант решения - embed
deploy
Тут все очевидно. Как автор dcape, деплой всех своих приложений я делаю так. Другие варианты - это уже подарки тестовых задач. Добавлю, что, на мой взгляд, навык заворачивания приложения в docker я считаю полезным для программиста, а опыт управления kubernetes - нет. По двум причинам (k8s не единственный и не последний. Разве это не экспертиза уровня webpack?)
errors
В точке возникновения ошибки коду доступен
- контекст
- аргументы вызова текущей функции
- стек
Эти данные важны разработчику, но не пользователю (которому хватит номера ошибки и текста).
Из этого я делаю вывод, что в момент отлова ошибки надо в лог положить все детали и переменные контекста, а наверх отдать какой-то примитив или ID. Но уж точно не надо весь контекст момента ошибки тащить наверх в полях ошибки.
cache
Если кешируем БД, для меня первейшая фича - если значения в кэше нет, обратиться к БД и поставить в очередь все такие же запросы. Я это нашел - groupcache
Смысл очевиден - не грузить БД одинаковыми запросами
Если нет работы с БД - все проще. Есть много вариантов и в какой-то момент я вышел на решение от patrickmn.
Следующий вариант сделал arp242, добавив к предыдущему дженерики. И это мой текущий выбор
generated code naming
На мой взгляд, есть плюсы в добавлении префикса “z” к имени файла, если он генерируется. Как минимум, это облегчает ревью, оставляя все такие правки на конец.
Вот пример из исходников Go.
Мое личное предпочтение по этой теме - выносить все генерируемое в папку /gen, как сделано тутю