GoLang Solutions

Ниже приведены мои варианты решения типовых задач, которые у меня возникали при разработке на golang.

func main()

Пока я не нашел удобный способ тестировать main(), мой вариант - сократить эту функцию до одной строки и убрать ее из тестов.

Если репозиторий содержит бинарник с именем APP, его функция main() размещается в отдельном файле cmd/APP/main.go со следующим содержанием:

1
2
3
4
5
6
7
8
9
//+build !test

// This file holds code which does not covered by tests

package main

import "os"

func main() { Run(os.Exit) }

log

  1. structured logging - чтобы парсить без геморроя, для прода это json stream. т.е. если в пакете логгирования есть строка формата - это сразу убираем
  2. поддержка терминала. Да, в проде хорош json, но при отладке хотелось бы цветной читабельный логгинг в терминал

Пока не было log/slog, я смотрел на тему так:

  • для случаев, отличных от примитивных, необходима возможность парсить логи приложения. Тут мой выбор - structured logging и вариант его реализации - zap
  • вместе с этим, зашивать конкретную имплементацию журналирования мне представляется не оптимальным, поэтому в пакетах использую logr
  • в тестах, если надо собрать логи и потом проанализировать, я использую genericr

Теперь ситуация изменилась

  • не осталось причин не использовать глобально пакет log/slog. Все его настройки можно сделать на старте, а работа с контекстом стала даже удобнее
  • Для логгирования контекста вместо log.FromContext использую slog.ErrorContext
  • если понадобится зависимость уровня логгирования от пакета - это решится в slog.Handler

log usage

Ссылка на log не передается конструктору и не является глобальной переменной. Она передается в конкретном вызове или через контекст.

Почему: В строку журнала может понадобиться добавить переменную со значением из текущего запроса (например - traceId, или еще что-то уровнем выше текущего пакета)

config

Для конфигурации приложения наиболее характерный пример для меня - использование библиотеки, у которой есть свой конфиг, при обновлении которого я бы не хотел ничего менять в своем приложении. Я использую go-flags и выглядит это так:

1
2
3
4
5
6
type Config struct {
        Addr        string `long:"http_addr" default:"localhost:8080"  description:"Http listen address"`

        FS  lookupfs.Config `group:"Filesystem Options" namespace:"fs" env-namespace:"FS"`
        API procapi.Config  `group:"API Options" namespace:"api" env-namespace:"API"`
}

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, как сделано тутю