Skip to content

Instantly share code, notes, and snippets.

@agalitsyn
Last active January 2, 2022 12:48
Show Gist options
  • Save agalitsyn/813c34591c794eb761e4d97d7d31624e to your computer and use it in GitHub Desktop.
Save agalitsyn/813c34591c794eb761e4d97d7d31624e to your computer and use it in GitHub Desktop.
golang anti patterns, examples from real codebases

golang anti patterns

Disclaimer: Несколько секций скопировано с презентации https://github.com/dlsniper/talks/tree/master/2017/go-anti-patterns-gdgnsk по причине готовых примеров. Все вещи, которые тут описаны, были встечены на настоящих code review.

Error handling

Не делайте:

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)
}

Panic

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 нужен для того, чтобы сказать последнее слово. Если у вас авария, надо записать в лог или успеть закрыть соединения и ответить что-то юзеру прежде чем умереть.

Изменение сигнатур функций из stdlib

Не делайте

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 и написать врапперы для врапперов.

Error handling in http handlers

Есть 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)

Перезапись имен пакетов из stdlib

Не делайте:

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

OOP

  • Не нужно писать метод к структуре, если вы используете 1 поле из структуры, передайте его аргументом в функцию. Это делает код более тестируемым и компонентным.
  • Капитанский совет, но пишите тесты сразу, как минимум на критичные части бизнес логики. Тесты помогают выявлять связный код сразу. Если для вашего теста надо создать 5 структур и замокать еще 10, то в вашем коде явно что-то не так.

Context

Не нужно использовать 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 могут превратить ваши программы фактически в набор акторов с определенной политикой коммуникации.

Testing

Не делайте

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/

Vendoring

Ответ на вопрос "вендорить или нет" однозначен: конечно, если вы ходите за пакетами на 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. Они наоборот за истину считают не твой код, а файл, в котором написано что и какой версии вендорить. Работает быстро и без магии

Пакеты

Router

Не берите 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, активно использует контекст.

Handlers

Есть хороший пакет github.com/gorilla/handlers, он достаточно старый и активно поддерживается, покрыт тестами. Фактические есть все необходимое для web API.

CLI

Выбор огромен, но обратите внимание на эти:

  • github.com/spf13/cobra - немного сложная библиотека, но используется в docker и kubernetes
  • github.com/alecthomas/kingpin - немного деревянная, но простая и понятная библиотека
  • github.com/urfave/cli - не вызывает особенных эмоций, нормально работает

Послушать http://golangshow.com/episode/2016/11-02-081/

Logger

Их много, список можно найти тут 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

Third-party библиотеки

Момент 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/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment