~/NeonXP.log

Uber Go Style Guide

Руководство по стилю Uber для Go

Оригинал: https://github.com/uber-go/guide/blob/master/style.md

Введение

Стили — это соглашения, которые регулируют наш код. Термин «стиль» немного неудачен, поскольку эти соглашения охватывают гораздо больше, чем просто форматирование исходных файлов — этим занимается gofmt.

Цель этого руководства — управлять этой сложностью, подробно описывая, что следует и чего не следует делать при написании кода на Go в Uber. Эти правила существуют, чтобы кодовая база оставалась управляемой, позволяя при этом инженерам эффективно использовать возможности языка Go.

Первоначально это руководство было создано Прашантом Варанаси и Саймоном Ньютоном как способ познакомить некоторых коллег с использованием Go. С годами оно было дополнено на основе отзывов других.

В этом документе описаны идиоматические соглашения в коде на Go, которым мы следуем в Uber. Многие из них являются общими рекомендациями для Go, в то время как другие расширяют внешние ресурсы:

  1. Effective Go
  2. Go Common Mistakes
  3. Go Code Review Comments

Мы стремимся к тому, чтобы примеры кода были корректны для двух последних минорных версий релизов Go.

Весь код должен быть свободен от ошибок при проверке с помощью golint и go vet. Мы рекомендуем настроить ваш редактор так, чтобы он:

  • Запускал goimports при сохранении
  • Запускал golint и go vet для проверки на ошибки

Информацию о поддержке инструментов Go в редакторах можно найти здесь: https://go.dev/wiki/IDEsAndTextEditorPlugins

Рекомендации

Указатели на интерфейсы

Почти никогда не нужен указатель на интерфейс. Следует передавать интерфейсы как значения — базовые данные при этом всё равно могут быть указателем.

Интерфейс состоит из двух полей:

  1. Указатель на информацию, специфичную для типа. Можно думать об этом как о «типе».
  2. Указатель на данные. Если хранимые данные являются указателем, они сохраняются напрямую. Если хранимые данные являются значением, то сохраняется указатель на это значение.

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

Проверка соответствия интерфейсу

Проверяйте соответствие интерфейсу на этапе компиляции там, где это уместно. Это включает:

  • Экспортируемые типы, которые должны реализовывать определённые интерфейсы как часть своего API-контракта
  • Экспортируемые или неэкспортируемые типы, которые являются частью коллекции типов, реализующих один и тот же интерфейс
  • Другие случаи, когда нарушение интерфейса приведёт к поломке пользователей
ПлохоХорошо
type Handler struct {
  // ...
}



func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  ...
}
type Handler struct {
  // ...
}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

Выражение var _ http.Handler = (*Handler)(nil) не скомпилируется, если *Handler перестанет соответствовать интерфейсу http.Handler.

Правая часть присваивания должна быть нулевым значением утверждаемого типа. Это nil для типов-указателей (например, *Handler), срезов и карт, и пустая структура для типов-структур.

type LogHandler struct {
  h   http.Handler
  log *zap.Logger
}

var _ http.Handler = LogHandler{}

func (h LogHandler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

Получатели и интерфейсы

Методы со значением-получателем могут вызываться как на указателях, так и на значениях. Методы с указателем-получателем могут вызываться только на указателях или адресуемых значениях.

Например,

type S struct {
  data string
}

func (s S) Read() string {
  return s.data
}

func (s *S) Write(str string) {
  s.data = str
}

// Мы не можем получить указатели на значения, хранящиеся в картах, потому что они не являются адресуемыми значениями.
sVals := map[int]S{1: {"A"}}

// Мы можем вызвать Read для значений, хранящихся в карте, потому что Read имеет получатель-значение, который не требует, чтобы значение было адресуемым.
sVals[1].Read()

// Мы не можем вызвать Write для значений, хранящихся в карте, потому что Write имеет получатель-указатель, и невозможно получить указатель на значение, хранящееся в карте.
//
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// Вы можете вызвать как Read, так и Write, если карта хранит указатели, потому что указатели по своей природе адресуемы.
sPtrs[1].Read()
sPtrs[1].Write("test")

Аналогично, интерфейс может быть удовлетворён указателем, даже если метод имеет получатель-значение.

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

// Следующее не скомпилируется, так как s2Val является значением, и для f нет получателя-значения.
//   i = s2Val

В Effective Go есть хорошее описание по теме Указатели против значений.

Нулевые значения мьютексов допустимы

Нулевое значение sync.Mutex и sync.RWMutex является допустимым, поэтому почти никогда не нужен указатель на мьютекс.

ПлохоХорошо
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()

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

ПлохоХорошо
type SMap struct {
  sync.Mutex

  data map[string]string
}

func NewSMap() *SMap {
  return &SMap{
    data: make(map[string]string),
  }
}

func (m *SMap) Get(k string) string {
  m.Lock()
  defer m.Unlock()

  return m.data[k]
}
type SMap struct {
  mu sync.Mutex

  data map[string]string
}

func NewSMap() *SMap {
  return &SMap{
    data: make(map[string]string),
  }
}

func (m *SMap) Get(k string) string {
  m.mu.Lock()
  defer m.mu.Unlock()

  return m.data[k]
}

Поле Mutex, а также методы Lock и Unlock непреднамеренно становятся частью публичного API SMap.

Мьютекс и его методы являются деталями реализации SMap, скрытыми от его вызывающих сторон.

Копируйте срезы и карты на границах

Срезы и карты содержат указатели на базовые данные, поэтому будьте осторожны в ситуациях, когда их нужно скопировать.

Получение срезов и карт

Помните, что пользователи могут изменить карту или срез, которые вы получили в качестве аргумента, если сохраните ссылку на них.

Плохо Хорошо
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// Вы хотели изменить d1.trips?
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// Теперь мы можем изменить trips[0], не затрагивая d1.trips.
trips[0] = ...

Возврат срезов и карт

Аналогично, будьте осторожны с модификациями карт или срезов, раскрывающих внутреннее состояние.

ПлохоХорошо
type Stats struct {
  mu sync.Mutex
  counters map[string]int
}

// Snapshot возвращает текущую статистику.
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  return s.counters
}

// snapshot больше не защищён мьютексом, поэтому любой доступ к snapshot подвержен состоянию гонки.
snapshot := stats.Snapshot()
type Stats struct {
  mu sync.Mutex
  counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  result := make(map[string]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// Snapshot теперь является копией.
snapshot := stats.Snapshot()

Используйте defer для очистки

Используйте defer для очистки ресурсов, таких как файлы и блокировки.

ПлохоХорошо
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// легко пропустить разблокировки из-за множественных возвратов
p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// более читаемо

defer имеет крайне малые накладные расходы, и его следует избегать только если вы можете доказать, что время выполнения вашей функции измеряется в наносекундах. Выигрыш в читаемости от использования defer стоит той мизерной стоимости, которую он вносит. Это особенно верно для больших методов, где присутствуют не только простые операции доступа к памяти, а другие вычисления более значимы, чем defer.

Размер канала — один или ноль

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

ПлохоХорошо
// Должно хватить на всех!
c := make(chan int, 64)
// Размер один
c := make(chan int, 1) // или
// Небуферизированный канал, размер ноль
c := make(chan int)

Начинайте перечисления с единицы

Стандартный способ введения перечислений в Go — объявление пользовательского типа и группы const с iota. Поскольку переменные имеют значение по умолчанию 0, обычно следует начинать перечисления с ненулевого значения.

ПлохоХорошо
type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

// Add=1, Subtract=2, Multiply=3

Бывают случаи, когда использование нулевого значения имеет смысл, например, когда случай с нулевым значением является желаемым поведением по умолчанию.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

Используйте "time" для работы со временем

Время — это сложно. Часто делаются неверные предположения о времени, включая следующие.

  1. В сутках 24 часа
  2. В часе 60 минут
  3. В неделе 7 дней
  4. В году 365 дней
  5. И многое другое

Например, 1 означает, что добавление 24 часов к моменту времени не всегда даст новый календарный день.

Поэтому всегда используйте пакет "time" при работе со временем, так как он помогает безопаснее и точнее справляться с этими неверными предположениями.

Используйте time.Time для моментов времени

Используйте time.Time при работе с моментами времени и методы time.Time для сравнения, добавления или вычитания времени.

ПлохоХорошо
func isActive(now, start, stop int) bool {
  return start <= now && now < stop
}
func isActive(now, start, stop time.Time) bool {
  return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}

Используйте time.Duration для промежутков времени

Используйте time.Duration при работе с промежутками времени.

ПлохоХорошо
func poll(delay int) {
  for {
    // ...
    time.Sleep(time.Duration(delay) * time.Millisecond)
  }
}

poll(10) // это секунды или миллисекунды?
func poll(delay time.Duration) {
  for {
    // ...
    time.Sleep(delay)
  }
}

poll(10*time.Second)

Возвращаясь к примеру добавления 24 часов к моменту времени, метод, который мы используем для добавления времени, зависит от намерения. Если мы хотим получить то же время суток, но на следующий календарный день, следует использовать Time.AddDate. Однако, если мы хотим момент времени, гарантированно наступающий через 24 часа после предыдущего, следует использовать Time.Add.

newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)

Используйте time.Time и time.Duration с внешними системами

По возможности используйте time.Duration и time.Time при взаимодействии с внешними системами. Например:

  • Флаги командной строки: flag поддерживает time.Duration через time.ParseDuration
  • JSON: encoding/json поддерживает кодирование time.Time как строки RFC 3339 через свой UnmarshalJSON метод
  • SQL: database/sql поддерживает преобразование столбцов DATETIME или TIMESTAMP в time.Time и обратно, если базовый драйвер поддерживает это.
  • YAML: gopkg.in/yaml.v2 поддерживает time.Time как строку RFC 3339 и time.Duration через time.ParseDuration.

Если невозможно использовать time.Duration в этих взаимодействиях, используйте int или float64 и включайте единицу измерения в имя поля.

Например, так как encoding/json не поддерживает time.Duration, единица измерения включается в имя поля.

ПлохоХорошо
// {"interval": 2}
type Config struct {
  Interval int `json:"interval"`
}
// {"intervalMillis": 2000}
type Config struct {
  IntervalMillis int `json:"intervalMillis"`
}

Если невозможно использовать time.Time в этих взаимодействиях, если не согласована альтернатива, используйте string и форматируйте временные метки в соответствии с RFC 3339. Этот формат используется по умолчанию в Time.UnmarshalText и доступен для использования в Time.Format и time.Parse через time.RFC3339.

Хотя на практике это обычно не проблема, имейте в виду, что пакет "time" не поддерживает разбор временных меток с високосными секундами (8728), а также не учитывает високосные секунды в вычислениях (15190). Если вы сравниваете два момента времени, разница не будет включать високосные секунды, которые могли произойти между этими моментами.

Ошибки

Типы ошибок

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

  • Нужно ли вызывающей стороне сопоставлять ошибку, чтобы обработать её? Если да, мы должны поддерживать функции errors.Is или errors.As путём объявления переменной ошибки верхнего уровня или пользовательского типа.
  • Сообщение об ошибке — статическая строка или динамическая строка, требующая контекстной информации? Для первого случая можно использовать errors.New, но для второго необходимо использовать fmt.Errorf или пользовательский тип ошибки.
  • Мы распространяем новую ошибку, возвращённую нижележащей функцией? Если да, см. раздел об обёртывании ошибок.
Сопоставление ошибок? Сообщение об ошибке Рекомендация
Нет статическое errors.New
Нет динамическое fmt.Errorf
Да статическое переменная верхнего уровня с errors.New
Да динамическое пользовательский тип error

Например, используйте errors.New для ошибки со статической строкой. Экспортируйте эту ошибку как переменную, чтобы поддерживать её сопоставление с errors.Is, если вызывающей стороне нужно сопоставить и обработать эту ошибку.

Без сопоставления ошибокС сопоставлением ошибок
// package foo

func Open() error {
  return errors.New("could not open")
}

// package bar

if err := foo.Open(); err != nil {
  // Не можем обработать ошибку.
  panic("unknown error")
}
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
  return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // обработать ошибку
  } else {
    panic("unknown error")
  }
}

Для ошибки с динамической строкой используйте fmt.Errorf, если вызывающей стороне не нужно её сопоставлять, и пользовательский error, если нужно.

Без сопоставления ошибокС сопоставлением ошибок
// package foo

func Open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

// package bar

if err := foo.Open("testfile.txt"); err != nil {
  // Не можем обработать ошибку.
  panic("unknown error")
}
// package foo

type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

func Open(file string) error {
  return &NotFoundError{File: file}
}


// package bar

if err := foo.Open("testfile.txt"); err != nil {
  var notFound *NotFoundError
  if errors.As(err, &notFound) {
    // обработать ошибку
  } else {
    panic("unknown error")
  }
}

Обратите внимание, что если вы экспортируете переменные или типы ошибок из пакета, они станут частью публичного API пакета.

Обёртывание ошибок

Есть три основных варианта распространения ошибок при неудачном вызове:

  • вернуть исходную ошибку как есть
  • добавить контекст с помощью fmt.Errorf и глагола %w
  • добавить контекст с помощью fmt.Errorf и глагола %v

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

В противном случае добавляйте контекст к сообщению об ошибке, где это возможно, чтобы вместо расплывчатой ошибки вроде “connection refused” вы получали более полезные ошибки, такие как “call service foo: connection refused”.

Используйте fmt.Errorf для добавления контекста к вашим ошибкам, выбирая между глаголами %w или %v в зависимости от того, должна ли вызывающая сторона иметь возможность сопоставить и извлечь базовую причину.

  • Используйте %w, если вызывающая сторона должна иметь доступ к базовой ошибке. Это хороший вариант по умолчанию для большинства обёрнутых ошибок, но имейте в виду, что вызывающие стороны могут начать полагаться на это поведение. Поэтому для случаев, когда обёрнутая ошибка является известной var или типом, документируйте и тестируйте это как часть контракта вашей функции.
  • Используйте %v, чтобы скрыть базовую ошибку. Вызывающие стороны не смогут её сопоставить, но вы сможете переключиться на %w в будущем, если потребуется.

При добавлении контекста к возвращаемым ошибкам сохраняйте контекст кратким, избегая фраз типа “failed to”, которые констатируют очевидное и накапливаются по мере всплытия ошибки по стеку:

ПлохоХорошо
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %w", err)
}
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %w", err)
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error

Однако, как только ошибка отправляется в другую систему, должно быть понятно, что сообщение является ошибкой (например, тег err или префикс “Failed” в логах).

См. также Don’t just check errors, handle them gracefully.

Именование ошибок

Для значений ошибок, хранящихся как глобальные переменные, используйте префикс Err или err в зависимости от того, экспортируются они или нет. Это указание заменяет Префикс _ для неэкспортируемых глобальных переменных.

var (
  // Следующие две ошибки экспортируются, чтобы пользователи этого пакета могли сопоставлять их с errors.Is.

  ErrBrokenLink = errors.New("link is broken")
  ErrCouldNotOpen = errors.New("could not open")

  // Эта ошибка не экспортируется, потому что мы не хотим делать её частью нашего публичного API. Мы всё ещё можем использовать её внутри пакета с errors.Is.

  errNotFound = errors.New("not found")
)

Для пользовательских типов ошибок используйте суффикс Error вместо этого.

// Аналогично, эта ошибка экспортируется, чтобы пользователи этого пакета могли сопоставлять её с errors.As.

type NotFoundError struct {
  File string
}
func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

// А эта ошибка не экспортируется, потому что мы не хотим делать её частью публичного API. Мы всё ещё можем использовать её внутри пакета с errors.As.

type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("resolve %q", e.Path)
}

Обрабатывайте ошибки один раз

Когда вызывающая сторона получает ошибку от вызываемой функции, она может обработать её различными способами в зависимости от того, что она знает об ошибке.

К ним относятся, но не ограничиваются:

  • если контракт вызываемой функции определяет конкретные ошибки, сопоставить ошибку с errors.Is или errors.As и обработать ветви по-разному
  • если ошибка является восстанавливаемой, залогировать ошибку и выполнить graceful degradation
  • если ошибка представляет собой отказ в предметной области, вернуть чётко определённую ошибку
  • вернуть ошибку, либо обёрнутую, либо как есть

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

Например, рассмотрим следующие случаи:

ОписаниеКод

Плохо: Логировать ошибку и возвращать её

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

u, err := getUser(id)
if err != nil {
  // ПЛОХО: см. описание
  log.Printf("Could not get user %q: %v", id, err)
  return err
}

Хорошо: Обернуть ошибку и вернуть её

Вызывающие стороны выше по стеку обработают ошибку. Использование %w гарантирует, что они смогут сопоставить ошибку с errors.Is или errors.As, если это уместно.

u, err := getUser(id)
if err != nil {
  return fmt.Errorf("get user %q: %w", id, err)
}

Хорошо: Логировать ошибку и выполнить graceful degradation

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

if err := emitMetrics(); err != nil {
  // Неудача при записи метрик не должна ломать приложение.
  log.Printf("Could not emit metrics: %v", err)
}

Хорошо: Сопоставить ошибку и выполнить graceful degradation

Если вызываемая функция определяет конкретную ошибку в своём контракте и сбой является восстанавливаемым, сопоставьте этот случай ошибки и выполните graceful degradation. Для всех остальных случаев оберните ошибку и верните её.

Вызывающие стороны выше по стеку обработают другие ошибки.

tz, err := getUserTimeZone(id)
if err != nil {
  if errors.Is(err, ErrUserNotFound) {
    // Пользователь не существует. Используем UTC.
    tz = time.UTC
  } else {
    return fmt.Errorf("get user %q: %w", id, err)
  }
}

Обрабатывайте сбои утверждения типа

Форма утверждения типа с одним возвращаемым значением вызовет панику при неверном типе. Поэтому всегда используйте идиому “comma ok”.

ПлохоХорошо
t := i.(string)
t, ok := i.(string)
if !ok {
  // обработать ошибку корректно
}

Не паникуйте

Код, работающий в production, должен избегать паник. Паники являются основной причиной каскадных сбоев. Если возникает ошибка, функция должна вернуть ошибку и позволить вызывающей стороне решить, как её обработать.

ПлохоХорошо
func run(args []string) {
  if len(args) == 0 {
    panic("an argument is required")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}
func run(args []string) error {
  if len(args) == 0 {
    return errors.New("an argument is required")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

Panic/recover — это не стратегия обработки ошибок. Программа должна паниковать только при возникновении чего-то невосстановимого, например, разыменовании nil. Исключением является инициализация программы: проблемы при запуске программы, которые должны её завершить, могут вызывать панику.

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

Даже в тестах предпочитайте t.Fatal или t.FailNow вместо паник, чтобы гарантировать, что тест будет помечен как неудачный.

ПлохоХорошо
// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  panic("failed to set up test")
}
// func TestFoo(t *testing.T)

f, err := os.CreateTemp("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}

Используйте go.uber.org/atomic

Атомарные операции с пакетом sync/atomic работают с базовыми типами (int32, int64 и т.д.), поэтому легко забыть использовать атомарную операцию для чтения или изменения переменных.

go.uber.org/atomic добавляет безопасность типов к этим операциям, скрывая базовый тип. Кроме того, он включает удобный тип atomic.Bool.

ПлохоХорошо
type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // уже работает…
     return
  }
  // запустить Foo
}

func (f *foo) isRunning() bool {
  return f.running == 1  // состояние гонки!
}
type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // уже работает…
     return
  }
  // запустить Foo
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}

Избегайте изменяемых глобальных переменных

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

ПлохоХорошо
// sign.go

var _timeNow = time.Now

func sign(msg string) string {
  now := _timeNow()
  return signWithTime(msg, now)
}
// sign.go

type signer struct {
  now func() time.Time
}

func newSigner() *signer {
  return &signer{
    now: time.Now,
  }
}

func (s *signer) Sign(msg string) string {
  now := s.now()
  return signWithTime(msg, now)
}
// sign_test.go

func TestSign(t *testing.T) {
  oldTimeNow := _timeNow
  _timeNow = func() time.Time {
    return someFixedTime
  }
  defer func() { _timeNow = oldTimeNow }()

  assert.Equal(t, want, sign(give))
}
// sign_test.go

func TestSigner(t *testing.T) {
  s := newSigner()
  s.now = func() time.Time {
    return someFixedTime
  }

  assert.Equal(t, want, s.Sign(give))
}

Избегайте встраивания типов в публичные структуры

Такие встроенные типы раскрывают детали реализации, препятствуют эволюции типов и затрудняют чтение документации.

Предположим, вы реализовали различные типы списков, используя общий AbstractList. Избегайте встраивания AbstractList в ваши конкретные реализации списков. Вместо этого напишите вручную только методы вашего конкретного списка, которые будут делегировать вызовы абстрактному списку.

type AbstractList struct {}

// Add добавляет сущность в список.
func (l *AbstractList) Add(e Entity) {
  // ...
}

// Remove удаляет сущность из списка.
func (l *AbstractList) Remove(e Entity) {
  // ...
}
ПлохоХорошо
// ConcreteList — это список сущностей.
type ConcreteList struct {
  *AbstractList
}
// ConcreteList — это список сущностей.
type ConcreteList struct {
  list *AbstractList
}

// Add добавляет сущность в список.
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}

// Remove удаляет сущность из списка.
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

Go позволяет встраивание типов как компромисс между наследованием и композицией. Внешний тип получает неявные копии методов встроенного типа. Эти методы по умолчанию делегируют вызовы тому же методу встроенного экземпляра.

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

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

Даже встраивание совместимого интерфейса AbstractList вместо структуры дало бы разработчику больше гибкости для изменений в будущем, но всё равно раскрыло бы деталь, что конкретные списки используют абстрактную реализацию.

ПлохоХорошо
// AbstractList — это обобщённая реализация для различных видов списков сущностей.
type AbstractList interface {
  Add(Entity)
  Remove(Entity)
}

// ConcreteList — это список сущностей.
type ConcreteList struct {
  AbstractList
}
// ConcreteList — это список сущностей.
type ConcreteList struct {
  list AbstractList
}

// Add добавляет сущность в список.
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}

// Remove удаляет сущность из списка.
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

И в случае со встроенной структурой, и со встроенным интерфейсом встроенный тип накладывает ограничения на эволюцию типа.

  • Добавление методов во встроенный интерфейс — это breaking change.
  • Удаление методов из встроенной структуры — это breaking change.
  • Удаление встроенного типа — это breaking change.
  • Замена встроенного типа, даже на альтернативу, удовлетворяющую тому же интерфейсу, — это breaking change.

Хотя написание этих делегирующих методов утомительно, дополнительные усилия скрывают деталь реализации, оставляют больше возможностей для изменений, а также устраняют косвенность при обнаружении полного интерфейса List в документации.

Избегайте использования встроенных имён

Спецификация языка Go описывает несколько встроенных, предобъявленных идентификаторов, которые не должны использоваться как имена в программах на Go.

В зависимости от контекста повторное использование этих идентификаторов в качестве имён либо затеняет оригинал в текущей лексической области видимости (и любых вложенных областях), либо делает затронутый код запутанным. В лучшем случае компилятор пожалуется; в худшем — такой код может привести к скрытым, трудноуловимым ошибкам.

ПлохоХорошо
var error string
// `error` затеняет встроенный идентификатор

// или

func handleErrorMessage(error string) {
    // `error` затеняет встроенный идентификатор
}
var errorMessage string
// `error` ссылается на встроенный идентификатор

// или

func handleErrorMessage(msg string) {
    // `error` ссылается на встроенный идентификатор
}
type Foo struct {
    // Хотя технически эти поля не создают затенение, поиск по строкам `error` или `string` теперь неоднозначен.
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` и `f.error` визуально похожи
    return f.error
}

func (f Foo) String() string {
    // `string` и `f.string` визуально похожи
    return f.string
}
type Foo struct {
    // `error` и `string` теперь однозначны.
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

Обратите внимание, что компилятор не будет генерировать ошибки при использовании предобъявленных идентификаторов, но такие инструменты, как go vet, должны корректно указывать на эти и другие случаи затенения.

Избегайте init()

Избегайте init() там, где это возможно. Когда init() неизбежен или желателен, код должен пытаться:

  1. Быть полностью детерминированным, независимо от среды программы или вызова.
  2. Избегать зависимости от порядка или побочных эффектов других функций init(). Хотя порядок init() хорошо известен, код может меняться, и поэтому зависимости между функциями init() могут сделать код хрупким и подверженным ошибкам.
  3. Избегать доступа или манипуляции глобальным состоянием или состоянием окружения, таким как информация о машине, переменные окружения, рабочая директория, аргументы/вводы программы и т.д.
  4. Избегать ввода-вывода, включая файловую систему, сеть и системные вызовы.

Код, который не может удовлетворить этим требованиям, вероятно, должен быть вспомогательной функцией, вызываемой как часть main() (или в другом месте жизненного цикла программы), или быть написан как часть самого main(). В частности, библиотеки, предназначенные для использования другими программами, должны особенно тщательно следить за полной детерминированностью и не выполнять “init magic”.

ПлохоХорошо
type Foo struct {
    // ...
}

var _defaultFoo Foo

func init() {
    _defaultFoo = Foo{
        // ...
    }
}
var _defaultFoo = Foo{
    // ...
}

// или, лучше, для тестируемости:

var _defaultFoo = defaultFoo()

func defaultFoo() Foo {
    return Foo{
        // ...
    }
}
type Config struct {
    // ...
}

var _config Config

func init() {
    // Плохо: зависит от текущей директории
    cwd, _ := os.Getwd()

    // Плохо: ввод-вывод
    raw, _ := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )

    yaml.Unmarshal(raw, &_config)
}
type Config struct {
    // ...
}

func loadConfig() Config {
    cwd, err := os.Getwd()
    // обработать err

    raw, err := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // обработать err

    var config Config
    yaml.Unmarshal(raw, &config)

    return config
}

Учитывая вышесказанное, некоторые ситуации, в которых init() может быть предпочтительнее или необходим, включают:

  • Сложные выражения, которые нельзя представить в виде одиночных присваиваний.
  • Подключаемые хуки, такие как диалекты database/sql, регистры типов кодирования и т.д.
  • Оптимизации для Google Cloud Functions и другие формы детерминированного предварительного вычисления.

Завершение программы в main

Программы на Go используют os.Exit или log.Fatal* для немедленного завершения. (Паника — нехороший способ завершения программ, пожалуйста, не паникуйте.)

Вызывайте os.Exit или log.Fatal* только в main(). Все остальные функции должны возвращать ошибки для сигнализации о сбое.

ПлохоХорошо
func main() {
  body := readFile(path)
  fmt.Println(body)
}

func readFile(path string) string {
  f, err := os.Open(path)
  if err != nil {
    log.Fatal(err)
  }

  b, err := io.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }

  return string(b)
}
func main() {
  body, err := readFile(path)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(body)
}

func readFile(path string) (string, error) {
  f, err := os.Open(path)
  if err != nil {
    return "", err
  }

  b, err := io.ReadAll(f)
  if err != nil {
    return "", err
  }

  return string(b), nil
}

Обоснование: Программы с несколькими функциями, которые завершают выполнение, создают несколько проблем:

  • Неочевидный поток управления: любая функция может завершить программу, поэтому становится трудно рассуждать о потоке управления.
  • Сложность тестирования: функция, завершающая программу, также завершит тест, который её вызывает. Это делает функцию трудной для тестирования и создаёт риск пропуска других тестов, которые ещё не были запущены go test.
  • Пропущенная очистка: когда функция завершает программу, она пропускает вызовы функций, поставленные в очередь с операторами defer. Это добавляет риск пропуска важных задач очистки.

Завершайте программу один раз

По возможности старайтесь вызывать os.Exit или log.Fatal не более одного раза в вашем main(). Если есть несколько сценариев ошибок, которые останавливают выполнение программы, поместите эту логику в отдельную функцию и возвращайте из неё ошибки.

Это приводит к сокращению функции main() и помещению всей ключевой бизнес-логики в отдельную, тестируемую функцию.

ПлохоХорошо
package main

func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("missing file")
  }
  name := args[0]

  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()

  // Если мы вызовем log.Fatal после этой строки, f.Close не будет вызван.

  b, err := io.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }

  // ...
}
package main

func main() {
  if err := run(); err != nil {
    log.Fatal(err)
  }
}

func run() error {
  args := os.Args[1:]
  if len(args) != 1 {
    return errors.New("missing file")
  }
  name := args[0]

  f, err := os.Open(name)
  if err != nil {
    return err
  }
  defer f.Close()

  b, err := io.ReadAll(f)
  if err != nil {
    return err
  }

  // ...
}

В примере выше используется log.Fatal, но рекомендация также применима к os.Exit или любому библиотечному коду, который вызывает os.Exit.

func main() {
  if err := run(); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

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

func main() {
  os.Exit(run(args))
}

func run() (exitCode int) {
  // ...
}

Более общо, обратите внимание, что функция run(), используемая в этих примерах, не является предписывающей. Существует гибкость в имени, сигнатуре и настройке функции run(). Среди прочего, вы можете:

  • принимать неразобранные аргументы командной строки (например, run(os.Args[1:]))
  • разбирать аргументы командной строки в main() и передавать их в run
  • использовать пользовательский тип ошибки для передачи кода выхода обратно в main()
  • помещать бизнес-логику на другой уровень абстракции, отличный от package main

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

Используйте теги полей в структурах для сериализации

Любое поле структуры, которое сериализуется в JSON, YAML или другие форматы, поддерживающие именование полей на основе тегов, должно быть аннотировано соответствующим тегом.

ПлохоХорошо
type Stock struct {
  Price int
  Name  string
}

bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})
type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
  // Безопасно переименовать Name в Symbol.
}

bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

Обоснование: Сериализованная форма структуры — это контракт между различными системами. Изменения в структуре сериализованной формы — включая имена полей — нарушают этот контракт. Указание имён полей внутри тегов делает контракт явным и защищает от случайного его нарушения при рефакторинге или переименовании полей.

Не запускайте горутины по принципу «запустил и забыл»

Горутины легковесны, но не бесплатны: как минимум, они требуют памяти для своего стека и процессорного времени для планирования. Хотя эти затраты малы для типичного использования горутин, они могут вызвать значительные проблемы с производительностью, если горутины создаются в больших количествах без контролируемого времени жизни. Горутины с неуправляемым временем жизни также могут вызывать другие проблемы, например, мешать сборке мусора для неиспользуемых объектов и удерживать ресурсы, которые в противном случае больше не используются.

Поэтому не допускайте утечек горутин в production коде. Используйте go.uber.org/goleak для тестирования утечек горутин внутри пакетов, которые могут их создавать.

В общем случае каждая горутина:

  • должна иметь предсказуемый момент, когда она перестанет выполняться; или
  • должен быть способ сигнализировать горутине, что ей следует остановиться

В обоих случаях должен быть способ заблокировать выполнение и дождаться завершения горутины.

Например:

ПлохоХорошо
go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()
var (
  stop = make(chan struct{}) // сигнализирует горутине остановиться
  done = make(chan struct{}) // сигнализирует нам, что горутина завершилась
)
go func() {
  defer close(done)

  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

// В другом месте...
close(stop)  // сигнализировать горутине остановиться
<-done       // и ждать её завершения

Нет способа остановить эту горутину. Она будет работать, пока приложение не завершится.

Эту горутину можно остановить с помощью close(stop), и мы можем дождаться её завершения с помощью <-done.

Дожидайтесь завершения горутин

Для горутины, созданной системой, должен быть способ дождаться её завершения. Есть два популярных способа сделать это:

  • Используйте sync.WaitGroup, чтобы дождаться завершения нескольких горутин. Делайте так, если нужно ждать несколько горутин.

    var wg sync.WaitGroup
    for i := 0; i < N; i++ {
      wg.Go(...)
    }
    
    // Чтобы дождаться завершения всех:
    wg.Wait()
    
  • Добавьте ещё один chan struct{}, который горутина закроет по завершении. Делайте так, если есть только одна горутина.

    done := make(chan struct{})
    go func() {
      defer close(done)
      // ...
    }()
    
    // Чтобы дождаться завершения горутины:
    <-done
    

Не используйте горутины в init()

Функции init() не должны запускать горутины. См. также Избегайте init().

Если пакету нужна фоновая горутина, он должен предоставлять объект, ответственный за управление временем жизни горутины. Этот объект должен предоставлять метод (Close, Stop, Shutdown и т.д.), который сигнализирует фоновой горутине об остановке и ждёт её завершения.

ПлохоХорошо
func init() {
  go doWork()
}

func doWork() {
  for {
    // ...
  }
}
type Worker struct{ /* ... */ }

func NewWorker(...) *Worker {
  w := &Worker{
    stop: make(chan struct{}),
    done: make(chan struct{}),
    // ...
  }
  go w.doWork()
}

func (w *Worker) doWork() {
  defer close(w.done)
  for {
    // ...
    case <-w.stop:
      return
  }
}

// Shutdown говорит воркеру остановиться и ждёт, пока он завершится.
func (w *Worker) Shutdown() {
  close(w.stop)
  <-w.done
}

Создаёт фоновую горутину безусловно при экспорте этого пакета пользователем. Пользователь не имеет контроля над горутиной или способа её остановки.

Создаёт воркера только если пользователь его запрашивает. Предоставляет способ остановки воркера, чтобы пользователь мог освободить используемые им ресурсы.

Обратите внимание, что следует использовать WaitGroup, если воркер управляет несколькими горутинами. См. Дожидайтесь завершения горутин.

Производительность

Рекомендации, специфичные для производительности, применяются только к «горячему пути» (hot path).

Предпочитайте strconv вместо fmt

При преобразовании примитивов в строки и обратно strconv быстрее, чем fmt.

ПлохоХорошо
for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}
for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}
BenchmarkFmtSprint-4    143 ns/op    2 allocs/op
BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

Избегайте повторного преобразования строк в байты

Не создавайте срезы байт из фиксированной строки повторно. Вместо этого выполните преобразование один раз и сохраните результат.

ПлохоХорошо
for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}
BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op

Предпочитайте указание ёмкости контейнеров

По возможности указывайте ёмкость контейнеров, чтобы выделить память для контейнера заранее. Это минимизирует последующие выделения памяти (из-за копирования и изменения размера контейнера) при добавлении элементов.

Указание подсказки ёмкости для карт

По возможности предоставляйте подсказку ёмкости при инициализации карт с помощью make().

make(map[T1]T2, hint)

Предоставление подсказки ёмкости для make() пытается правильно определить размер карты при инициализации, что уменьшает необходимость её роста и выделений памяти при добавлении элементов.

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

ПлохоХорошо
m := make(map[string]os.FileInfo)

files, _ := os.ReadDir("./files")
for _, f := range files {
    m[f.Name()] = f
}

files, _ := os.ReadDir("./files")

m := make(map[string]os.DirEntry, len(files))
for _, f := range files {
    m[f.Name()] = f
}

m создаётся без подсказки размера; при присваивании может быть больше выделений памяти.

m создаётся с подсказкой размера; при присваивании может быть меньше выделений памяти.

Указание ёмкости срезов

По возможности предоставляйте подсказку ёмкости при инициализации срезов с помощью make(), особенно при использовании append.

make([]T, length, capacity)

В отличие от карт, ёмкость среза — это не подсказка: компилятор выделит достаточно памяти для ёмкости среза, предоставленной в make(), что означает, что последующие операции append() не будут приводить к выделениям памяти (пока длина среза не совпадёт с ёмкостью, после чего любые добавления потребуют изменения размера для хранения дополнительных элементов).

ПлохоХорошо
for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
BenchmarkBad-4    100000000    2.48s
BenchmarkGood-4   100000000    0.21s

Стиль

Избегайте слишком длинных строк

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

Мы рекомендуем мягкое ограничение длины строки в 99 символов. Авторы должны стараться переносить строки до достижения этого предела, но это не строгое ограничение. Коду разрешено превышать этот лимит.

Будьте последовательны

Некоторые рекомендации, изложенные в этом документе, можно оценить объективно; другие ситуативны, контекстны или субъективны.

Прежде всего, будьте последовательны.

Последовательный код легче поддерживать, легче осмыслить, требует меньше когнитивных усилий и легче переносить или обновлять по мере появления новых соглашений или исправления классов ошибок.

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

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

Группируйте схожие объявления

Go поддерживает группировку схожих объявлений.

ПлохоХорошо
import "a"
import "b"
import (
  "a"
  "b"
)

Это также относится к константам, переменным и объявлениям типов.

ПлохоХорошо

const a = 1
const b = 2



var a = 1
var b = 2



type Area float64
type Volume float64
const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

Группируйте только связанные объявления. Не группируйте несвязанные объявления.

ПлохоХорошо
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

Группы не ограничены в том, где могут использоваться. Например, их можно использовать внутри функций.

ПлохоХорошо
func f() string {
  red := color.New(0xff0000)
  green := color.New(0x00ff00)
  blue := color.New(0x0000ff)

  // ...
}
func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  // ...
}

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

ПлохоХорошо
func (c *client) request() {
  caller := c.name
  format := "json"
  timeout := 5*time.Second
  var err error

  // ...
}
func (c *client) request() {
  var (
    caller  = c.name
    format  = "json"
    timeout = 5*time.Second
    err error
  )

  // ...
}

Порядок групп импорта

Должно быть две группы импорта:

  • Стандартная библиотека
  • Все остальные

Это группировка, применяемая по умолчанию в goimports.

ПлохоХорошо
import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)
import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Имена пакетов

При именовании пакетов выбирайте имя, которое:

  • Состоит только из строчных букв. Без заглавных букв и подчёркиваний.
  • Не требует переименования с использованием именованных импортов в большинстве мест вызова.
  • Короткое и ёмкое. Помните, что имя полностью указывается в каждом месте вызова.
  • Не во множественном числе. Например, net/url, а не net/urls.
  • Не “common”, “util”, “shared” или “lib”. Это плохие, неинформативные имена.

См. также Package Names и Style guideline for Go packages.

Имена функций

Мы следуем соглашению сообщества Go об использовании MixedCaps для имён функций. Исключение делается для тестовых функций, которые могут содержать подчёркивания для группировки связанных тестовых случаев, например, TestMyFunction_WhatIsBeingTested.

Псевдонимы импорта

Псевдонимы импорта должны использоваться, если имя пакета не совпадает с последним элементом пути импорта.

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

Во всех остальных сценариях псевдонимы импорта следует избегать, если нет прямого конфликта между импортами.

ПлохоХорошо
import (
  "fmt"
  "os"
  runtimetrace "runtime/trace"

  nettrace "golang.net/x/trace"
)
import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

Группировка и порядок функций

  • Функции должны быть отсортированы в приблизительном порядке вызовов.
  • Функции в файле должны быть сгруппированы по получателю.

Следовательно, экспортируемые функции должны появляться первыми в файле после определений struct, const, var.

newXYZ()/NewXYZ() может появиться после определения типа, но до остальных методов получателя.

Поскольку функции группируются по получателю, простые вспомогательные функции должны появляться ближе к концу файла.

ПлохоХорошо
func (s *something) Cost() {
  return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []int) int {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}
type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
  return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []int) int {...}

Уменьшайте вложенность

Код должен уменьшать вложенность, где это возможно, обрабатывая случаи ошибок/особые условия первыми и возвращаясь рано или продолжая цикл. Уменьшайте количество кода, вложенного на несколько уровней.

ПлохоХорошо
for _, v := range data {
  if v.F1 == 1 {
    v = process(v)
    if err := v.Call(); err == nil {
      v.Send()
    } else {
      return err
    }
  } else {
    log.Printf("Invalid v: %v", v)
  }
}
for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

Избыточный Else

Если переменная устанавливается в обеих ветках if, это можно заменить одним if.

ПлохоХорошо
var a int
if b {
  a = 100
} else {
  a = 10
}
a := 10
if b {
  a = 100
}

Объявления переменных верхнего уровня

На верхнем уровне используйте стандартное ключевое слово var. Не указывайте тип, если он не отличается от типа выражения.

ПлохоХорошо
var _s string = F()

func F() string { return "A" }
var _s = F()
// Поскольку F уже указывает, что возвращает строку, нам не нужно снова указывать тип.

func F() string { return "A" }

Указывайте тип, если тип выражения не совпадает точно с желаемым типом.

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F возвращает объект типа myError, но мы хотим error.

Префикс _ для неэкспортируемых глобальных переменных

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

Обоснование: Переменные и константы верхнего уровня имеют область видимости пакета. Использование общего имени делает лёгким случайное использование неправильного значения в другом файле.

ПлохоХорошо
// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)

  // Мы не увидим ошибку компиляции, если первая строка Bar() будет удалена.
}
// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

Исключение: Неэкспортируемые значения ошибок могут использовать префикс err без подчёркивания. См. Именование ошибок.

Встраивание в структурах

Встроенные типы должны находиться в начале списка полей структуры, и должна быть пустая строка, отделяющая встроенные поля от обычных полей.

ПлохоХорошо
type Client struct {
  version int
  http.Client
}
type Client struct {
  http.Client

  version int
}

Встраивание должно обеспечивать ощутимую пользу, например, добавлять или расширять функциональность семантически-уместным способом. Оно должно делать это без каких-либо негативных последствий для пользователя (см. также: Избегайте встраивания типов в публичные структуры).

Исключение: Мьютексы не должны встраиваться, даже в неэкспортируемые типы. См. также: Нулевые значения мьютексов допустимы.

Встраивание НЕ должно:

  • Быть чисто косметическим или ориентированным на удобство.
  • Усложнять создание или использование внешних типов.
  • Влиять на нулевые значения внешних типов. Если внешний тип имеет полезное нулевое значение, он должен сохранять его после встраивания внутреннего типа.
  • Раскрывать несвязанные функции или поля внешнего типа как побочный эффект встраивания внутреннего типа.
  • Раскрывать неэкспортируемые типы.
  • Влиять на семантику копирования внешних типов.
  • Менять API или семантику типов внешних типов.
  • Встраивать неканоническую форму внутреннего типа.
  • Раскрывать детали реализации внешнего типа.
  • Позволять пользователям наблюдать или контролировать внутренности типа.
  • Менять общее поведение внутренних функций через обёртывание таким образом, который может удивить пользователей.

Проще говоря, встраивайте осознанно и преднамеренно. Хороший тест: “все ли эти экспортируемые внутренние методы/поля были бы добавлены напрямую к внешнему типу”; если ответ “некоторые” или “нет”, не встраивайте внутренний тип — используйте поле.

ПлохоХорошо
type A struct {
    // Плохо: A.Lock() и A.Unlock() теперь доступны, не предоставляют функциональной пользы и позволяют пользователям контролировать детали внутренностей A.
    sync.Mutex
}
type countingWriteCloser struct {
    // Хорошо: Write() предоставлен на этом внешнем уровне для конкретной цели и делегирует работу Write() внутреннего типа.
    io.WriteCloser

    count int
}

func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}
type Book struct {
    // Плохо: указатель меняет полезность нулевого значения
    io.ReadWriter

    // другие поля
}

// позже
var b Book
b.Read(...)  // panic: nil pointer
b.String()   // panic: nil pointer
b.Write(...) // panic: nil pointer
type Book struct {
    // Хорошо: имеет полезное нулевое значение
    bytes.Buffer

    // другие поля
}

// позже

var b Book
b.Read(...)  // ok
b.String()   // ok
b.Write(...) // ok
type Client struct {
    sync.Mutex
    sync.WaitGroup
    bytes.Buffer
    url.URL
}
type Client struct {
    mtx sync.Mutex
    wg  sync.WaitGroup
    buf bytes.Buffer
    url url.URL
}

Объявление локальных переменных

Короткое объявление переменных (:=) должно использоваться, если переменная явно устанавливается в некоторое значение.

ПлохоХорошо
var s = "foo"
s := "foo"

Однако бывают случаи, когда значение по умолчанию понятнее при использовании ключевого слова var. Объявление пустых срезов, например.

ПлохоХорошо
func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}
func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

nil — это валидный срез

nil — это валидный срез длины 0. Это означает, что:

  • Не следует явно возвращать срез длины ноль. Возвращайте nil вместо этого.

    ПлохоХорошо
    if x == "" {
      return []int{}
    }
    
    if x == "" {
      return nil
    }
    
  • Чтобы проверить, пуст ли срез, всегда используйте len(s) == 0. Не проверяйте на nil.

    ПлохоХорошо
    func isEmpty(s []string) bool {
      return s == nil
    }
    
    func isEmpty(s []string) bool {
      return len(s) == 0
    }
    
  • Нулевое значение (срез, объявленный с var) можно использовать сразу без make().

    ПлохоХорошо
    nums := []int{}
    // или, nums := make([]int)
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }
    
    var nums []int
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }
    

    Помните, что хотя nil-срез является валидным срезом, он не эквивалентен выделенному срезу длины 0 — один является nil, а другой нет — и они могут обрабатываться по-разному в разных ситуациях (например, при сериализации).

    Уменьшайте область видимости переменных

    По возможности уменьшайте область видимости переменных и констант. Не уменьшайте область видимости, если это противоречит Уменьшению вложенности.

    ПлохоХорошо
    err := os.WriteFile(name, data, 0644)
    if err != nil {
     return err
    }
    
    if err := os.WriteFile(name, data, 0644); err != nil {
     return err
    }
    

    Если вам нужен результат вызова функции вне if, то не следует пытаться уменьшить область видимости.

    ПлохоХорошо
    if data, err := os.ReadFile(name); err == nil {
      err = cfg.Decode(data)
      if err != nil {
        return err
      }
    
      fmt.Println(cfg)
      return nil
    } else {
      return err
    }
    
    data, err := os.ReadFile(name)
    if err != nil {
       return err
    }
    
    if err := cfg.Decode(data); err != nil {
      return err
    }
    
    fmt.Println(cfg)
    return nil
    

    Константам не нужно быть глобальными, если они не используются в нескольких функциях или файлах или являются частью внешнего контракта пакета.

    ПлохоХорошо
    const (
      _defaultPort = 8080
      _defaultUser = "user"
    )
    
    func Bar() {
      fmt.Println("Default port", _defaultPort)
    }
    
    func Bar() {
      const (
        defaultPort = 8080
        defaultUser = "user"
      )
      fmt.Println("Default port", defaultPort)
    }
    

    Избегайте «голых» параметров

    «Голые» параметры в вызовах функций могут ухудшать читаемость. Добавляйте комментарии в стиле C (/* ... */) для имён параметров, когда их значение неочевидно.

    ПлохоХорошо
    // func printInfo(name string, isLocal, done bool)
    
    printInfo("foo", true, true)
    
    // func printInfo(name string, isLocal, done bool)
    
    printInfo("foo", true /* isLocal */, true /* done */)
    

    Ещё лучше заменить «голые» типы bool пользовательскими типами для более читаемого и типобезопасного кода. Это позволяет в будущем иметь более двух состояний (true/false) для этого параметра.

    type Region int
    
    const (
      UnknownRegion Region = iota
      Local
    )
    
    type Status int
    
    const (
      StatusReady Status = iota + 1
      StatusDone
      // Возможно, в будущем у нас будет StatusInProgress.
    )
    
    func printInfo(name string, region Region, status Status)
    

    Используйте сырые строковые литералы, чтобы избежать экранирования

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

    ПлохоХорошо
    wantError := "unknown name:\"test\""
    
    wantError := `unknown error:"test"`
    

    Инициализация структур

    Используйте имена полей для инициализации структур

    Вы почти всегда должны указывать имена полей при инициализации структур. Теперь это обеспечивается go vet.

    ПлохоХорошо
    k := User{"John", "Doe", true}
    
    k := User{
        FirstName: "John",
        LastName: "Doe",
        Admin: true,
    }
    

    Исключение: Имена полей могут быть опущены в таблицах тестов, когда полей 3 или меньше.

    tests := []struct{
      op Operation
      want string
    }{
      {Add, "add"},
      {Subtract, "subtract"},
    }
    

    Опускайте поля с нулевыми значениями в структурах

    При инициализации структур с именами полей опускайте поля, имеющие нулевые значения, если они не предоставляют значимый контекст. В противном случае позвольте Go автоматически установить их в нулевые значения.

    ПлохоХорошо
    user := User{
      FirstName: "John",
      LastName: "Doe",
      MiddleName: "",
      Admin: false,
    }
    
    user := User{
      FirstName: "John",
      LastName: "Doe",
    }
    

    Это помогает уменьшить шум для читателей, опуская значения, которые являются стандартными в данном контексте. Указываются только значимые значения.

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

    tests := []struct{
      give string
      want int
    }{
      {give: "0", want: 0},
      // ...
    }
    

    Используйте var для структур с нулевыми значениями

    Когда все поля структуры опущены в объявлении, используйте форму var для объявления структуры.

    ПлохоХорошо
    user := User{}
    
    var user User
    

    Это отличает структуры с нулевыми значениями от тех, у которых есть ненулевые поля, аналогично различию, создаваемому для инициализации карт, и соответствует тому, как мы предпочитаем объявлять пустые срезы.

    Инициализация ссылок на структуры

    Используйте &T{} вместо new(T) при инициализации ссылок на структуры, чтобы это было согласовано с инициализацией структур.

    ПлохоХорошо
    sval := T{Name: "foo"}
    
    // несогласованно
    sptr := new(T)
    sptr.Name = "bar"
    
    sval := T{Name: "foo"}
    
    sptr := &T{Name: "bar"}
    

    Инициализация карт

    Предпочитайте make(..) для пустых карт и карт, заполняемых программно. Это делает инициализацию карт визуально отличной от объявления и позволяет легко добавить подсказку размера позже, если она доступна.

    ПлохоХорошо
    var (
      // m1 безопасна для чтения и записи;
      // m2 вызовет панику при записи.
      m1 = map[T1]T2{}
      m2 map[T1]T2
    )
    
    var (
      // m1 безопасна для чтения и записи;
      // m2 вызовет панику при записи.
      m1 = make(map[T1]T2)
      m2 map[T1]T2
    )
    

    Объявление и инициализация визуально похожи.

    Объявление и инициализация визуально различны.

    По возможности предоставляйте подсказку ёмкости при инициализации карт с помощью make(). См. Указание подсказки ёмкости для карт для получения дополнительной информации.

    С другой стороны, если карта содержит фиксированный список элементов, используйте литералы карт для её инициализации.

    ПлохоХорошо
    m := make(map[T1]T2, 3)
    m[k1] = v1
    m[k2] = v2
    m[k3] = v3
    
    m := map[T1]T2{
      k1: v1,
      k2: v2,
      k3: v3,
    }
    

    Основное правило — использовать литералы карт при добавлении фиксированного набора элементов во время инициализации, в противном случае используйте make (и указывайте подсказку размера, если доступна).

    Строки формата вне Printf

    Если вы объявляете строки формата для функций в стиле Printf вне строкового литерала, сделайте их значениями const.

    Это помогает go vet выполнять статический анализ строки формата.

    ПлохоХорошо
    msg := "unexpected values %v, %v\n"
    fmt.Printf(msg, 1, 2)
    
    const msg = "unexpected values %v, %v\n"
    fmt.Printf(msg, 1, 2)
    

    Именование функций в стиле Printf

    Когда вы объявляете функцию в стиле Printf, убедитесь, что go vet может её обнаружить и проверить строку формата.

    Это означает, что следует использовать предопределённые имена функций в стиле Printf, если это возможно. go vet проверяет их по умолчанию. См. Printf family для получения дополнительной информации.

    Если использование предопределённых имён невозможно, заканчивайте выбранное вами имя на f: Wrapf, а не Wrap. go vet можно попросить проверять определённые имена в стиле Printf, но они должны заканчиваться на f.

    go vet -printfuncs=wrapf,statusf
    

    См. также go vet: Printf family check.

    Паттерны

    Табличные тесты

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

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

    ПлохоХорошо
    // func TestSplitHostPort(t *testing.T)
    
    host, port, err := net.SplitHostPort("192.0.2.0:8000")
    require.NoError(t, err)
    assert.Equal(t, "192.0.2.0", host)
    assert.Equal(t, "8000", port)
    
    host, port, err = net.SplitHostPort("192.0.2.0:http")
    require.NoError(t, err)
    assert.Equal(t, "192.0.2.0", host)
    assert.Equal(t, "http", port)
    
    host, port, err = net.SplitHostPort(":8000")
    require.NoError(t, err)
    assert.Equal(t, "", host)
    assert.Equal(t, "8000", port)
    
    host, port, err = net.SplitHostPort("1:8")
    require.NoError(t, err)
    assert.Equal(t, "1", host)
    assert.Equal(t, "8", port)
    
    // func TestSplitHostPort(t *testing.T)
    
    tests := []struct{
      give     string
      wantHost string
      wantPort string
    }{
      {
        give:     "192.0.2.0:8000",
        wantHost: "192.0.2.0",
        wantPort: "8000",
      },
      {
        give:     "192.0.2.0:http",
        wantHost: "192.0.2.0",
        wantPort: "http",
      },
      {
        give:     ":8000",
        wantHost: "",
        wantPort: "8000",
      },
      {
        give:     "1:8",
        wantHost: "1",
        wantPort: "8",
      },
    }
    
    for _, tt := range tests {
      t.Run(tt.give, func(t *testing.T) {
        host, port, err := net.SplitHostPort(tt.give)
        require.NoError(t, err)
        assert.Equal(t, tt.wantHost, host)
        assert.Equal(t, tt.wantPort, port)
      })
    }
    

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

    Мы следуем соглашению, что срез структур называется tests, а каждый тестовый случай — tt. Кроме того, мы рекомендуем явно указывать входные и выходные значения для каждого тестового случая с префиксами give и want.

    tests := []struct{
      give     string
      wantHost string
      wantPort string
    }{
      // ...
    }
    
    for _, tt := range tests {
      // ...
    }
    

    Избегайте излишней сложности в табличных тестах

    Табличные тесты могут быть трудны для чтения и поддержки, если подтесты содержат условные проверки или другую разветвлённую логику. Табличные тесты НЕ ДОЛЖНЫ использоваться всякий раз, когда внутри подтестов требуется сложная или условная логика (т.е. сложная логика внутри цикла for).

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

    Такие табличные тесты следует разделить либо на несколько таблиц тестов, либо на несколько отдельных функций Test....

    К некоторым идеалам, к которым стоит стремиться, относятся:

    • Фокусировка на самой узкой единице поведения
    • Минимизация «глубины теста» и избегание условных проверок (см. ниже)
    • Обеспечение того, что все поля таблицы используются во всех тестах
    • Обеспечение того, что вся тестовая логика выполняется для всех случаев таблицы

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

    Конкретно, табличные тесты могут стать запутанными и трудными для чтения, если они используют несколько ветвящихся путей (например, shouldError, expectCall и т.д.), используют много операторов if для специфичных ожиданий моков (например, shouldCallFoo) или размещают функции внутри таблицы (например, setupMocks func(*FooMock)).

    Однако при тестировании поведения, которое меняется только в зависимости от изменённых входных данных, может быть предпочтительнее группировать схожие случаи вместе в табличном тесте, чтобы лучше иллюстрировать, как поведение меняется при всех входных данных, а не разделять иначе сопоставимые единицы на отдельные тесты и делать их более трудными для сравнения и противопоставления.

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

    ПлохоХорошо
    func TestComplicatedTable(t *testing.T) {
      tests := []struct {
        give          string
        want          string
        wantErr       error
        shouldCallX   bool
        shouldCallY   bool
        giveXResponse string
        giveXErr      error
        giveYResponse string
        giveYErr      error
      }{
        // ...
      }
    
      for _, tt := range tests {
        t.Run(tt.give, func(t *testing.T) {
          // настройка моков
          ctrl := gomock.NewController(t)
          xMock := xmock.NewMockX(ctrl)
          if tt.shouldCallX {
            xMock.EXPECT().Call().Return(
              tt.giveXResponse, tt.giveXErr,
            )
          }
          yMock := ymock.NewMockY(ctrl)
          if tt.shouldCallY {
            yMock.EXPECT().Call().Return(
              tt.giveYResponse, tt.giveYErr,
            )
          }
    
          got, err := DoComplexThing(tt.give, xMock, yMock)
    
          // проверка результатов
          if tt.wantErr != nil {
            require.EqualError(t, err, tt.wantErr)
            return
          }
          require.NoError(t, err)
          assert.Equal(t, want, got)
        })
      }
    }
    
    func TestShouldCallX(t *testing.T) {
      // настройка моков
      ctrl := gomock.NewController(t)
      xMock := xmock.NewMockX(ctrl)
      xMock.EXPECT().Call().Return("XResponse", nil)
    
      yMock := ymock.NewMockY(ctrl)
    
      got, err := DoComplexThing("inputX", xMock, yMock)
    
      require.NoError(t, err)
      assert.Equal(t, "want", got)
    }
    
    func TestShouldCallYAndFail(t *testing.T) {
      // настройка моков
      ctrl := gomock.NewController(t)
      xMock := xmock.NewMockX(ctrl)
    
      yMock := ymock.NewMockY(ctrl)
      yMock.EXPECT().Call().Return("YResponse", nil)
    
      _, err := DoComplexThing("inputY", xMock, yMock)
      assert.EqualError(t, err, "Y failed")
    }
    

    Эта сложность делает тест более трудным для изменения, понимания и доказательства его корректности.

    Хотя строгих правил нет, читаемость и поддерживаемость всегда должны быть главными при выборе между табличными тестами и отдельными тестами для множественных входов/выходов системы.

    Параллельные тесты

    Параллельные тесты, как и некоторые специализированные циклы (например, те, что создают горутины или захватывают ссылки как часть тела цикла), должны заботиться о явном присваивании переменных цикла внутри области видимости цикла, чтобы гарантировать, что они содержат ожидаемые значения.

    tests := []struct{
      give string
      // ...
    }{
      // ...
    }
    
    for _, tt := range tests {
      tt := tt // для t.Parallel
      t.Run(tt.give, func(t *testing.T) {
        t.Parallel()
        // ...
      })
    }
    

    В примере выше мы должны объявить переменную tt с областью видимости итерации цикла из-за использования t.Parallel() ниже. Если мы этого не сделаем, большинство или все тесты получат неожиданное значение для tt или значение, которое меняется во время их выполнения.

    Функциональные опции

    Функциональные опции — это паттерн, в котором вы объявляете непрозрачный тип Option, который записывает информацию в некоторую внутреннюю структуру. Вы принимаете переменное количество этих опций и действуете на основе полной информации, записанной опциями во внутренней структуре.

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

    ПлохоХорошо
    // package db
    
    func Open(
      addr string,
      cache bool,
      logger *zap.Logger
    ) (*Connection, error) {
      // ...
    }
    
    // package db
    
    type Option interface {
      // ...
    }
    
    func WithCache(c bool) Option {
      // ...
    }
    
    func WithLogger(log *zap.Logger) Option {
      // ...
    }
    
    // Open создаёт соединение.
    func Open(
      addr string,
      opts ...Option,
    ) (*Connection, error) {
      // ...
    }
    

    Параметры cache и logger всегда должны предоставляться, даже если пользователь хочет использовать значения по умолчанию.

    db.Open(addr, db.DefaultCache, zap.NewNop())
    db.Open(addr, db.DefaultCache, log)
    db.Open(addr, false /* cache */, zap.NewNop())
    db.Open(addr, false /* cache */, log)
    

    Опции предоставляются только при необходимости.

    db.Open(addr)
    db.Open(addr, db.WithLogger(log))
    db.Open(addr, db.WithCache(false))
    db.Open(
      addr,
      db.WithCache(false),
      db.WithLogger(log),
    )
    

    Наш рекомендуемый способ реализации этого паттерна — использование интерфейса Option с неэкспортируемым методом, записывающим опции в неэкспортируемую структуру options.

    type options struct {
      cache  bool
      logger *zap.Logger
    }
    
    type Option interface {
      apply(*options)
    }
    
    type cacheOption bool
    
    func (c cacheOption) apply(opts *options) {
      opts.cache = bool(c)
    }
    
    func WithCache(c bool) Option {
      return cacheOption(c)
    }
    
    type loggerOption struct {
      Log *zap.Logger
    }
    
    func (l loggerOption) apply(opts *options) {
      opts.logger = l.Log
    }
    
    func WithLogger(log *zap.Logger) Option {
      return loggerOption{Log: log}
    }
    
    // Open создаёт соединение.
    func Open(
      addr string,
      opts ...Option,
    ) (*Connection, error) {
      options := options{
        cache:  defaultCache,
        logger: zap.NewNop(),
      }
    
      for _, o := range opts {
        o.apply(&options)
      }
    
      // ...
    }
    

    Обратите внимание, что существует метод реализации этого паттерна с использованием замыканий, но мы считаем, что паттерн выше предоставляет больше гибкости авторам и легче отлаживается и тестируется пользователями. В частности, он позволяет сравнивать опции друг с другом в тестах и моках, в отличие от замыканий, где это невозможно. Кроме того, он позволяет опциям реализовывать другие интерфейсы, включая fmt.Stringer, что позволяет создавать удобочитаемые строковые представления опций.

    См. также,

    Линтинг

    Более важно, чем любой «благословленный» набор линтеров, — линтить последовательно по всей кодовой базе.

    Мы рекомендуем использовать следующие линтеры как минимум, потому что считаем, что они помогают выявить наиболее распространённые проблемы, а также устанавливают высокую планку качества кода, не будучи излишне предписывающими:

    • errcheck для обеспечения обработки ошибок
    • goimports для форматирования кода и управления импортами
    • golint для указания на распространённые стилевые ошибки
    • govet для анализа кода на распространённые ошибки
    • staticcheck для выполнения различных проверок статического анализа

    Запускатели линтеров

    Мы рекомендуем golangci-lint в качестве основного запускателя линтеров для кода на Go, во многом благодаря его производительности в больших кодовых базах и возможности настраивать и использовать многие канонические линтеры одновременно. Этот репозиторий содержит пример .golangci.yml файла конфигурации с рекомендуемыми линтерами и настройками.

    golangci-lint имеет различные линтеры, доступные для использования. Вышеуказанные линтеры рекомендуются в качестве базового набора, и мы поощряем команды добавлять любые дополнительные линтеры, которые имеют смысл для их проектов.