Disclaimer: Несколько секций скопировано с презентации https://github.com/dlsniper/talks/tree/master/2017/go-anti-patterns-gdgnsk по причине готовых примеров. Все вещи, которые тут описаны, были встечены на настоящих code review.
Не делайте:
func DontErr() error {
return fmt.Errorf("some error")
}
И так не делайте:
func DontErr() error {
return errors.New("some error")
}
Делайте:
// ErrSome
var ErrSome = errors.New("some error")
func DoErr() error {
return ErrSome
}
Почему?
func Demo() {
if DoErr() == ErrSome {
fmt.Println("Some error")
}
}
Пример
package main
import (
"log"
"github.com/pkg/errors"
)
var ErrFooIsBar = errors.New("foo is bar")
func main() {
if err := iRaiseAnError("bad string"); err != nil {
if errors.Cause(err) == ErrFooIsBar {
log.Fatalf("Fatal error: %v", err)
}
log.Printf("Error: %v", err)
}
}
func iRaiseAnError(p string) error {
return errors.Wrapf(ErrFooIsBar, "could not proceed with arg (%v)", p)
}
https://golang.org/doc/effective_go.html#panic
Не делайте:
body, err := json.Marshal(message)
if err != nil {
panic(err)
}
Делайте:
body, err := json.Marshal(message)
if err != nil {
return err
}
Почему?
Ну нужно смешивать ожидаемые ошибки программы (не смог получить ответ по API или не смог открыть файл) с аварийными ошибками (параллельная запись в map, взятие отсутвуещего элемента по индексу из slice). Паттерн panic - recover нужен для того, чтобы сказать последнее слово. Если у вас авария, надо записать в лог или успеть закрыть соединения и ответить что-то юзеру прежде чем умереть.
Не делайте
type HandlerFunc func(ctx *apicontext.Context, w http.ResponseWriter, r *http.Request)
Допустимо
type handlerFunc func(w http.ResponseWriter, r *http.Request) error
func makeHandler(handler handlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := handler(w, r)
if err != nil {
trace.WriteError(w, err)
}
}
}
Почему?
Такой хендлер вызовется через обертку, которая приводит его к http.HandlerFunc. Нестандартная сигнатура хендлеров, которя к тому же требует свой сontext это непременно источник боли. Попытка использовать middleware или свою библиотеку требует заэкспозитъ этот context и написать врапперы для врапперов.
Есть 2 распространенных механизма:
- пример выше показывает, как изменить сигнатуру http handler для того чтобы возвращать ошибку. Эту ошибку может перехватить middleware и сформировать ответ от сервера с нужным http кодом. Подобный подход используется в echo https://echo.labstack.com/guide/error-handling
- другой вариант - создание функций по нужному http статусу, которые будут возвращать ответ. Сигнатура хендлера не меняется, после вызова такой функции делается return, как вот тут https://github.com/go-chi/chi/blob/master/_examples/rest/main.go#L419
Мне больше нравится вариант 2, потому что он ближе к stdlib и в явном виде показывает как хендлиться ошибка и что отвечает сервер:
if err := m.Delete(article); err != nil {
logger.WithError(err).Error()
render.Render(w, r, response.ErrUnknown(err))
return
}
render.NoContent(w, r)
Не делайте:
import (
_time "time"
"github.com/foo/bar/api/context"
"github.com/foo/bar/time"
)
Делайте:
import (
"time"
"context"
"github.com/foo/bar/pkg/contextutil"
"github.com/foo/bar/pkg/timeutil"
)
Почему?
Не вызывайте у ваших коллег когнитивный диссонанс. Помещайте функции-утилиты в util библиотеки, они не являются заменой настоящего package из stdlib.
Допустимо:
import (
log "github.com/Sirupsen/logrus"
)
Почему?
logrus.logger совместим с log.Logger
- Не нужно писать метод к структуре, если вы используете 1 поле из структуры, передайте его аргументом в функцию. Это делает код более тестируемым и компонентным.
- Капитанский совет, но пишите тесты сразу, как минимум на критичные части бизнес логики. Тесты помогают выявлять связный код сразу. Если для вашего теста надо создать 5 структур и замокать еще 10, то в вашем коде явно что-то не так.
Не нужно использовать context как dependecy injection контейнер, потому что "сontext is for cancelation". https://dave.cheney.net/2017/01/26/context-is-for-cancelation
Полезный туториал - https://www.youtube.com/watch?v=LSzR0VEraWw&t=3s
Многочисленные доказательства и примеры кода вы можете найти в https://github.com/grpc/grpc-go. Propagation и cancelation в context могут превратить ваши программы фактически в набор акторов с определенной политикой коммуникации.
Не делайте
err := b.Validate()
assert.NoError(t, err)
Делайте
if err := b.Validate(); err != nil {
t.Fatalf(err)
}
Почему?
У вас должна быть возможность прочитать текст ошибки и понять почему тест упал. Стандартный пакет testing содержит нужные функции для того, чтобы пометить тесты упавшими
Еще совет - используйте table-driven тесты, для того, чтобы подавать много разных входных и выходных параметров, что увеличивает корректность. Так же, если у вас есть синхронизация или каналы, попробуйте параллельные тесты и запускайте их с включенным race detector, подробнее тут https://rakyll.org/parallelize-test-tables/
Ответ на вопрос "вендорить или нет" однозначен: конечно, если вы ходите за пакетами на github. Не обязательно, если у вас есть все форки или кеш в вашем любимом gitlab, artifactory, другое.
Если вы вендорите: не нужно напрягаться по поводу комита vendor в VSC. Это дает несколько преимуществ:
- самодостаточный репозиторий, go get + make и у вас есть бинарник
- CI быстрее, не нужно выкачивать зависимости
- стабильность в сборке выше, опять же потому что не надо ходить в github, который не всегда работает
- при обновлении библиотеки можно легко посмотреть diff в git и отследить изменения сигнатур и прочие не совместимые вещи
- легко отслеживать, то что вы случайно изменили библиотеку, когда отлаживали
- при переходе с ветки на ветку у вас работает сборка, не надо постоянно синкать пакеты
- Все ждут dep, потому что это оффициальная тулза
- godep устарел, хотя и используется много где. Лучше не берите, много глюков и он портит ваш GOPATH
- govendor полет нормальный
- glide полет нормальный, но пользоваться страшно. Он слишком умный и не понятно что делает
- 2 других тула - https://github.com/rancher/trash и https://github.com/LK4D4/vndr. Они наоборот за истину считают не твой код, а файл, в котором написано что и какой версии вендорить. Работает быстро и без магии
Не берите github.com/julienschmidt/httprouter
Почему?
github.com/gorilla/mux
использует context для передачи аргументов в хендлер- mux не изменяет сигнатуру хендлера, об этом написано выше. Можно добавить любые middleware, в том числе 3rd-party.
- вы скажете, что в httprouter тоже можно добавить все что угодно, но через врапперы. Я не вижу смысла нагромождать эту лапшу, потому что 90% сервисов не нуждаются в zero allocation router. Если вы пишете такой сервис, что у вас произошел затык по performance в router, то вы не читаете эту статью, а работаете где-то в фейсбуке или гугле
Еще можно посмотреть github.com/pressly/chi
- роутер написанный уже после go1.7, активно использует контекст.
Есть хороший пакет github.com/gorilla/handlers
, он достаточно старый и активно поддерживается, покрыт тестами. Фактические есть все необходимое для web API.
Выбор огромен, но обратите внимание на эти:
- github.com/spf13/cobra - немного сложная библиотека, но используется в docker и kubernetes
- github.com/alecthomas/kingpin - немного деревянная, но простая и понятная библиотека
- github.com/urfave/cli - не вызывает особенных эмоций, нормально работает
Послушать http://golangshow.com/episode/2016/11-02-081/
Их много, список можно найти тут http://golangshow.com/episode/2016/10-14-077/ и про все логгеры послушать.
От себя добавлю, что предпочитайте:
github.com/sirupsen/logrus
- подходит для 99% проектов, есть куча адаптеров во всевозможные сборщики логовgithub.com/hashicorp/logutils
- норм для небольших проектов, очень минималистично- стандартный логгер - если вам не нужны уровни, вполне себе норм
Не нужно пытаться вкорячить github.com/uber-go/zap
, а потом жаловаться, что у вас логгер в проекте писали хищники для чужих. Так же не советую github.com/apex/log
, изначально проектировался как более легкий логрус, но вообще не развивается.
Про оформление пакетов читаем тут - https://rakyll.org/style-packages/
Если у вас > 1 бинарника, то посмотрите структуру
.
├── cmd
│ └── foo
│ └── main.go
├── pkg
│ └── logutil
│ └── log.go
├── README.md
└── vendor
В cmd по папкам лежит то, что соберется в бинарники. Там будет package main
и какой-то специфический код для конкретной програмы. В pkg лежат общие библиотеки, утилиты, которые эти программы могут использовать.
Почему нельзя фигачить все в 1 папке? Можно, если это небольшой проект. Но даже не слишком большие проекты включают в себя целый ряд активностей
- парсинг CLI аргументов
- сбор конфигурации приложения
- прокидывание кусков этих конфигов к другим подсистемам приложения
- создания разных сущностей, логгера, веб-сервера, конекта к база данных
- метрики
- обработка сигналов и завершение программы
- и тд
В период всеобщей докеризации есть много программ, которые конфигурируется только с помощью env. Особенно доставляет, когда у них переменные PORT а не PROGRAM_NAME_PORT и запустить их не в докере на 1 хосте вообще никак.
Момент 1 - если я пишу программу foo и хочу конфигурировать ее через env переменные, то нужно задать им префикс в виде FOO_DATABASE_URL, а не DATABASE_URL
Момент 2 - предпочитайте собирать опции приложения через CLI c возможностью использовать env. Приведенные выше библиотеки это умеют. Это делает вашу программу универсальной и удобной в запуске хоть в докере, хоть в консоли локально.
Момент 3 - не всегда ваш бинарник сразу должен демонизироваться, иногда там есть еще команды, типо почистить кеш, выполнить миграции. Здесь вам уже не обойтись без CLI
Момент 1 - затащенная раз библиотека очень тяжело может быть вытащена потом Момент 2 - если вам нужна 1 функция, то не надо скачивать leftpad библиотеку. Просто скопируйте функцию к себе в проект (это не стремно) Момент 3 - When adopting third-party libraries, default mindset should be “convince me this is better than us writing it” Момент 4 - Не смотрите на звезды на github, посмотрите покрытие тестами и наличие нормального API в библиотеке в первую очередь. Если это транспорт - то соотвествие с RFC
Читать https://dave.cheney.net/
Смотреть https://www.youtube.com/channel/UC_BzFbxG2za3bp5NRRRXJSw
Слушать http://golangshow.com/
Чатиться http://4gophers.ru/slack/