--- order: 10 title: Uber Go Style Guide --- # Руководство по стилю Uber для Go Оригинал: https://github.com/uber-go/guide/blob/master/style.md - [Введение](#введение) - [Рекомендации](#рекомендации) - [Указатели на интерфейсы](#указатели-на-интерфейсы) - [Проверка соответствия интерфейсу](#проверка-соответствия-интерфейсу) - [Получатели и интерфейсы](#получатели-и-интерфейсы) - [Нулевые значения мьютексов допустимы](#нулевые-значения-мьютексов-допустимы) - [Копируйте срезы и карты на границах](#копируйте-срезы-и-карты-на-границах) - [Используйте `defer` для очистки](#используйте-defer-для-очистки) - [Размер канала — один или ноль](#размер-канала-один-или-ноль) - [Начинайте перечисления с единицы](#начинайте-перечисления-с-единицы) - [Используйте `"time"` для работы со временем](#используйте-time-для-работы-со-временем) - [Ошибки](#ошибки) - [Типы ошибок](#типы-ошибок) - [Обёртывание ошибок](#обёртывание-ошибок) - [Именование ошибок](#именование-ошибок) - [Обрабатывайте ошибки один раз](#обрабатывайте-ошибки-один-раз) - [Обрабатывайте сбои утверждения типа](#обрабатывайте-сбои-утверждения-типа) - [Не паникуйте](#не-паникуйте) - [Используйте go.uber.org/atomic](#используйте-gouberorgatomic) - [Избегайте изменяемых глобальных переменных](#избегайте-изменяемых-глобальных-переменных) - [Избегайте встраивания типов в публичные структуры](#избегайте-встраивания-типов-в-публичные-структуры) - [Избегайте использования встроенных имён](#избегайте-использования-встроенных-имён) - [Избегайте `init()`](#избегайте-init) - [Завершение программы в main](#завершение-программы-в-main) - [Завершайте программу один раз](#завершайте-программу-один-раз) - [Используйте теги полей в структурах для сериализации](#используйте-теги-полей-в-структурах-для-сериализации) - [Не запускайте горутины по принципу «запустил и забыл»](#не-запускайте-горутины-по-принципу-запустил-и-забыл) - [Дожидайтесь завершения горутин](#дожидайтесь-завершения-горутин) - [Не используйте горутины в `init()`](#не-используйте-горутины-в-init) - [Производительность](#производительность) - [Предпочитайте strconv вместо fmt](#предпочитайте-strconv-вместо-fmt) - [Избегайте повторного преобразования строк в байты](#избегайте-повторного-преобразования-строк-в-байты) - [Предпочитайте указание ёмкости контейнеров](#предпочитайте-указание-ёмкости-контейнеров) - [Стиль](#стиль) - [Избегайте слишком длинных строк](#избегайте-слишком-длинных-строк) - [Будьте последовательны](#будьте-последовательны) - [Группируйте схожие объявления](#группируйте-схожие-объявления) - [Порядок групп импорта](#порядок-групп-импорта) - [Имена пакетов](#имена-пакетов) - [Имена функций](#имена-функций) - [Псевдонимы импорта](#псевдонимы-импорта) - [Группировка и порядок функций](#группировка-и-порядок-функций) - [Уменьшайте вложенность](#уменьшайте-вложенность) - [Избыточный Else](#избыточный-else) - [Объявления переменных верхнего уровня](#объявления-переменных-верхнего-уровня) - [Префикс \_ для неэкспортируемых глобальных переменных](#префикс-_-для-неэкспортируемых-глобальных-переменных) - [Встраивание в структурах](#встраивание-в-структурах) - [Объявление локальных переменных](#объявление-локальных-переменных) - [nil — это валидный срез](#nil-это-валидный-срез) - [Уменьшайте область видимости переменных](#уменьшайте-область-видимости-переменных) - [Избегайте «голых» параметров](#избегайте-голых-параметров) - [Используйте сырые строковые литералы, чтобы избежать экранирования](#используйте-сырые-строковые-литералы-чтобы-избежать-экранирования) - [Инициализация структур](#инициализация-структур) - [Используйте имена полей для инициализации структур](#используйте-имена-полей-для-инициализации-структур) - [Опускайте поля с нулевыми значениями в структурах](#опускайте-поля-с-нулевыми-значениями-в-структурах) - [Используйте `var` для структур с нулевыми значениями](#используйте-var-для-структур-с-нулевыми-значениями) - [Инициализация ссылок на структуры](#инициализация-ссылок-на-структуры) - [Инициализация карт](#инициализация-карт) - [Строки формата вне Printf](#строки-формата-вне-printf) - [Именование функций в стиле Printf](#именование-функций-в-стиле-printf) - [Паттерны](#паттерны) - [Табличные тесты](#табличные-тесты) - [Функциональные опции](#функциональные-опции) - [Линтинг](#линтинг) ## Введение Стили — это соглашения, которые регулируют наш код. Термин «стиль» немного неудачен, поскольку эти соглашения охватывают гораздо больше, чем просто форматирование исходных файлов — этим занимается `gofmt`. Цель этого руководства — управлять этой сложностью, подробно описывая, что следует и чего не следует делать при написании кода на Go в Uber. Эти правила существуют, чтобы кодовая база оставалась управляемой, позволяя при этом инженерам эффективно использовать возможности языка Go. Первоначально это руководство было создано [Прашантом Варанаси](https://github.com/prashantv) и [Саймоном Ньютоном](https://github.com/nomis52) как способ познакомить некоторых коллег с использованием Go. С годами оно было дополнено на основе отзывов других. В этом документе описаны идиоматические соглашения в коде на Go, которым мы следуем в Uber. Многие из них являются общими рекомендациями для Go, в то время как другие расширяют внешние ресурсы: 1. [Effective Go](https://go.dev/doc/effective_go) 2. [Go Common Mistakes](https://go.dev/wiki/CommonMistakes) 3. [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) Мы стремимся к тому, чтобы примеры кода были корректны для двух последних минорных версий [релизов Go](https://go.dev/doc/devel/release). Весь код должен быть свободен от ошибок при проверке с помощью `golint` и `go vet`. Мы рекомендуем настроить ваш редактор так, чтобы он: - Запускал `goimports` при сохранении - Запускал `golint` и `go vet` для проверки на ошибки Информацию о поддержке инструментов Go в редакторах можно найти здесь: https://go.dev/wiki/IDEsAndTextEditorPlugins ## Рекомендации ### Указатели на интерфейсы Почти никогда не нужен указатель на интерфейс. Следует передавать интерфейсы как значения — базовые данные при этом всё равно могут быть указателем. Интерфейс состоит из двух полей: 1. Указатель на информацию, специфичную для типа. Можно думать об этом как о «типе». 2. Указатель на данные. Если хранимые данные являются указателем, они сохраняются напрямую. Если хранимые данные являются значением, то сохраняется указатель на это значение. Если нужно, чтобы методы интерфейса изменяли базовые данные, необходимо использовать указатель. ### Проверка соответствия интерфейсу Проверяйте соответствие интерфейсу на этапе компиляции там, где это уместно. Это включает: - Экспортируемые типы, которые должны реализовывать определённые интерфейсы как часть своего API-контракта - Экспортируемые или неэкспортируемые типы, которые являются частью коллекции типов, реализующих один и тот же интерфейс - Другие случаи, когда нарушение интерфейса приведёт к поломке пользователей
ПлохоХорошо
```go type Handler struct { // ... } func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { ... } ``` ```go 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`), срезов и карт, и пустая структура для типов-структур. ```go type LogHandler struct { h http.Handler log *zap.Logger } var _ http.Handler = LogHandler{} func (h LogHandler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { // ... } ``` ### Получатели и интерфейсы Методы со значением-получателем могут вызываться как на указателях, так и на значениях. Методы с указателем-получателем могут вызываться только на указателях или [адресуемых значениях](https://go.dev/ref/spec#Method_values). Например, ```go 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") ``` Аналогично, интерфейс может быть удовлетворён указателем, даже если метод имеет получатель-значение. ```go 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 есть хорошее описание по теме [Указатели против значений](https://go.dev/doc/effective_go#pointers_vs_values). ### Нулевые значения мьютексов допустимы Нулевое значение `sync.Mutex` и `sync.RWMutex` является допустимым, поэтому почти никогда не нужен указатель на мьютекс.
ПлохоХорошо
```go mu := new(sync.Mutex) mu.Lock() ``` ```go var mu sync.Mutex mu.Lock() ```
Если вы используете структуру по указателю, то мьютекс должен быть не указателем, а полем в ней. Не встраивайте мьютекс в структуру, даже если структура не экспортируется.
ПлохоХорошо
```go 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] } ``` ```go 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`, скрытыми от его вызывающих сторон.
### Копируйте срезы и карты на границах Срезы и карты содержат указатели на базовые данные, поэтому будьте осторожны в ситуациях, когда их нужно скопировать. #### Получение срезов и карт Помните, что пользователи могут изменить карту или срез, которые вы получили в качестве аргумента, если сохраните ссылку на них.
Плохо Хорошо
```go func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := ... d1.SetTrips(trips) // Вы хотели изменить d1.trips? trips[0] = ... ``` ```go 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] = ... ```
#### Возврат срезов и карт Аналогично, будьте осторожны с модификациями карт или срезов, раскрывающих внутреннее состояние.
ПлохоХорошо
```go 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() ``` ```go 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` для очистки ресурсов, таких как файлы и блокировки.
ПлохоХорошо
```go p.Lock() if p.count < 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount // легко пропустить разблокировки из-за множественных возвратов ``` ```go p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // более читаемо ```
`defer` имеет крайне малые накладные расходы, и его следует избегать только если вы можете доказать, что время выполнения вашей функции измеряется в наносекундах. Выигрыш в читаемости от использования `defer` стоит той мизерной стоимости, которую он вносит. Это особенно верно для больших методов, где присутствуют не только простые операции доступа к памяти, а другие вычисления более значимы, чем `defer`. ### Размер канала — один или ноль Каналы обычно должны иметь размер один или быть небуферизированными. По умолчанию каналы небуферизированы и имеют размер ноль. Любой другой размер должен подвергаться тщательному анализу. Подумайте, как определяется размер, что предотвращает заполнение канала под нагрузкой и блокировку писателей, и что происходит, когда это случается.
ПлохоХорошо
```go // Должно хватить на всех! c := make(chan int, 64) ``` ```go // Размер один c := make(chan int, 1) // или // Небуферизированный канал, размер ноль c := make(chan int) ```
### Начинайте перечисления с единицы Стандартный способ введения перечислений в Go — объявление пользовательского типа и группы `const` с `iota`. Поскольку переменные имеют значение по умолчанию 0, обычно следует начинать перечисления с ненулевого значения.
ПлохоХорошо
```go type Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2 ``` ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) // Add=1, Subtract=2, Multiply=3 ```
Бывают случаи, когда использование нулевого значения имеет смысл, например, когда случай с нулевым значением является желаемым поведением по умолчанию. ```go 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. [И многое другое](https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time) Например, _1_ означает, что добавление 24 часов к моменту времени не всегда даст новый календарный день. Поэтому всегда используйте пакет [`"time"`](https://pkg.go.dev/time) при работе со временем, так как он помогает безопаснее и точнее справляться с этими неверными предположениями. #### Используйте `time.Time` для моментов времени Используйте [`time.Time`](https://pkg.go.dev/time#Time) при работе с моментами времени и методы `time.Time` для сравнения, добавления или вычитания времени.
ПлохоХорошо
```go func isActive(now, start, stop int) bool { return start <= now && now < stop } ``` ```go func isActive(now, start, stop time.Time) bool { return (start.Before(now) || start.Equal(now)) && now.Before(stop) } ```
#### Используйте `time.Duration` для промежутков времени Используйте [`time.Duration`](https://pkg.go.dev/time#Duration) при работе с промежутками времени.
ПлохоХорошо
```go func poll(delay int) { for { // ... time.Sleep(time.Duration(delay) * time.Millisecond) } } poll(10) // это секунды или миллисекунды? ``` ```go func poll(delay time.Duration) { for { // ... time.Sleep(delay) } } poll(10*time.Second) ```
Возвращаясь к примеру добавления 24 часов к моменту времени, метод, который мы используем для добавления времени, зависит от намерения. Если мы хотим получить то же время суток, но на следующий календарный день, следует использовать [`Time.AddDate`](https://pkg.go.dev/time#Time.AddDate). Однако, если мы хотим момент времени, гарантированно наступающий через 24 часа после предыдущего, следует использовать [`Time.Add`](https://pkg.go.dev/time#Time.Add). ```go newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */) maybeNewDay := t.Add(24 * time.Hour) ``` #### Используйте `time.Time` и `time.Duration` с внешними системами По возможности используйте `time.Duration` и `time.Time` при взаимодействии с внешними системами. Например: - Флаги командной строки: [`flag`](https://pkg.go.dev/flag) поддерживает `time.Duration` через [`time.ParseDuration`](https://pkg.go.dev/time#ParseDuration) - JSON: [`encoding/json`](https://pkg.go.dev/encoding/json) поддерживает кодирование `time.Time` как строки [RFC 3339](https://tools.ietf.org/html/rfc3339) через свой [`UnmarshalJSON` метод](https://pkg.go.dev/time#Time.UnmarshalJSON) - SQL: [`database/sql`](https://pkg.go.dev/database/sql) поддерживает преобразование столбцов `DATETIME` или `TIMESTAMP` в `time.Time` и обратно, если базовый драйвер поддерживает это. - YAML: [`gopkg.in/yaml.v2`](https://pkg.go.dev/gopkg.in/yaml.v2) поддерживает `time.Time` как строку [RFC 3339](https://tools.ietf.org/html/rfc3339) и `time.Duration` через [`time.ParseDuration`](https://pkg.go.dev/time#ParseDuration). Если невозможно использовать `time.Duration` в этих взаимодействиях, используйте `int` или `float64` и включайте единицу измерения в имя поля. Например, так как `encoding/json` не поддерживает `time.Duration`, единица измерения включается в имя поля.
ПлохоХорошо
```go // {"interval": 2} type Config struct { Interval int `json:"interval"` } ``` ```go // {"intervalMillis": 2000} type Config struct { IntervalMillis int `json:"intervalMillis"` } ```
Если невозможно использовать `time.Time` в этих взаимодействиях, если не согласована альтернатива, используйте `string` и форматируйте временные метки в соответствии с [RFC 3339](https://tools.ietf.org/html/rfc3339). Этот формат используется по умолчанию в [`Time.UnmarshalText`](https://pkg.go.dev/time#Time.UnmarshalText) и доступен для использования в `Time.Format` и `time.Parse` через [`time.RFC3339`](https://pkg.go.dev/time#RFC3339). Хотя на практике это обычно не проблема, имейте в виду, что пакет `"time"` не поддерживает разбор временных меток с високосными секундами ([8728](https://github.com/golang/go/issues/8728)), а также не учитывает високосные секунды в вычислениях ([15190](https://github.com/golang/go/issues/15190)). Если вы сравниваете два момента времени, разница не будет включать високосные секунды, которые могли произойти между этими моментами. ### Ошибки #### Типы ошибок Есть несколько вариантов объявления ошибок. Рассмотрите следующее, прежде чем выбрать вариант, наиболее подходящий для вашего случая. - Нужно ли вызывающей стороне сопоставлять ошибку, чтобы обработать её? Если да, мы должны поддерживать функции [`errors.Is`](https://pkg.go.dev/errors#Is) или [`errors.As`](https://pkg.go.dev/errors#As) путём объявления переменной ошибки верхнего уровня или пользовательского типа. - Сообщение об ошибке — статическая строка или динамическая строка, требующая контекстной информации? Для первого случая можно использовать [`errors.New`](https://pkg.go.dev/errors#New), но для второго необходимо использовать [`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf) или пользовательский тип ошибки. - Мы распространяем новую ошибку, возвращённую нижележащей функцией? Если да, см. [раздел об обёртывании ошибок](#обёртывание-ошибок). | Сопоставление ошибок? | Сообщение об ошибке | Рекомендация | | --------------------- | ------------------- | -------------------------------------------------------------------------- | | Нет | статическое | [`errors.New`](https://pkg.go.dev/errors#New) | | Нет | динамическое | [`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf) | | Да | статическое | переменная верхнего уровня с [`errors.New`](https://pkg.go.dev/errors#New) | | Да | динамическое | пользовательский тип `error` | Например, используйте [`errors.New`](https://pkg.go.dev/errors#New) для ошибки со статической строкой. Экспортируйте эту ошибку как переменную, чтобы поддерживать её сопоставление с `errors.Is`, если вызывающей стороне нужно сопоставить и обработать эту ошибку.
Без сопоставления ошибокС сопоставлением ошибок
```go // package foo func Open() error { return errors.New("could not open") } // package bar if err := foo.Open(); err != nil { // Не можем обработать ошибку. panic("unknown error") } ``` ```go // 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`](https://pkg.go.dev/fmt#Errorf), если вызывающей стороне не нужно её сопоставлять, и пользовательский `error`, если нужно.
Без сопоставления ошибокС сопоставлением ошибок
```go // 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") } ``` ```go // 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, ¬Found) { // обработать ошибку } 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", которые констатируют очевидное и накапливаются по мере всплытия ошибки по стеку:
ПлохоХорошо
```go s, err := store.New() if err != nil { return fmt.Errorf( "failed to create new store: %w", err) } ``` ```go s, err := store.New() if err != nil { return fmt.Errorf( "new store: %w", err) } ```
```plain failed to x: failed to y: failed to create new store: the error ``` ```plain x: y: new store: the error ```
Однако, как только ошибка отправляется в другую систему, должно быть понятно, что сообщение является ошибкой (например, тег `err` или префикс "Failed" в логах). См. также [Don't just check errors, handle them gracefully](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully). #### Именование ошибок Для значений ошибок, хранящихся как глобальные переменные, используйте префикс `Err` или `err` в зависимости от того, экспортируются они или нет. Это указание заменяет [Префикс \_ для неэкспортируемых глобальных переменных](#префикс-_-для-неэкспортируемых-глобальных-переменных). ```go var ( // Следующие две ошибки экспортируются, чтобы пользователи этого пакета могли сопоставлять их с errors.Is. ErrBrokenLink = errors.New("link is broken") ErrCouldNotOpen = errors.New("could not open") // Эта ошибка не экспортируется, потому что мы не хотим делать её частью нашего публичного API. Мы всё ещё можем использовать её внутри пакета с errors.Is. errNotFound = errors.New("not found") ) ``` Для пользовательских типов ошибок используйте суффикс `Error` вместо этого. ```go // Аналогично, эта ошибка экспортируется, чтобы пользователи этого пакета могли сопоставлять её с 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 - если ошибка представляет собой отказ в предметной области, вернуть чётко определённую ошибку - вернуть ошибку, либо [обёрнутую](#обёртывание-ошибок), либо как есть Независимо от того, как вызывающая сторона обрабатывает ошибку, обычно она должна обрабатывать каждую ошибку только один раз. Вызывающая сторона не должна, например, логировать ошибку, а затем возвращать её, потому что _её_ вызывающие стороны тоже могут обработать ошибку. Например, рассмотрим следующие случаи:
ОписаниеКод
**Плохо**: Логировать ошибку и возвращать её Вызывающие стороны выше по стеку, вероятно, предпримут аналогичные действия с ошибкой. Это приведёт к большому количеству шума в логах приложения при малой пользе. ```go u, err := getUser(id) if err != nil { // ПЛОХО: см. описание log.Printf("Could not get user %q: %v", id, err) return err } ```
**Хорошо**: Обернуть ошибку и вернуть её Вызывающие стороны выше по стеку обработают ошибку. Использование `%w` гарантирует, что они смогут сопоставить ошибку с `errors.Is` или `errors.As`, если это уместно. ```go u, err := getUser(id) if err != nil { return fmt.Errorf("get user %q: %w", id, err) } ```
**Хорошо**: Логировать ошибку и выполнить graceful degradation Если операция не является строго необходимой, мы можем обеспечить деградировавший, но работающий опыт, восстановившись после ошибки. ```go if err := emitMetrics(); err != nil { // Неудача при записи метрик не должна ломать приложение. log.Printf("Could not emit metrics: %v", err) } ```
**Хорошо**: Сопоставить ошибку и выполнить graceful degradation Если вызываемая функция определяет конкретную ошибку в своём контракте и сбой является восстанавливаемым, сопоставьте этот случай ошибки и выполните graceful degradation. Для всех остальных случаев оберните ошибку и верните её. Вызывающие стороны выше по стеку обработают другие ошибки. ```go 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".
ПлохоХорошо
```go t := i.(string) ``` ```go t, ok := i.(string) if !ok { // обработать ошибку корректно } ```
### Не паникуйте Код, работающий в production, должен избегать паник. Паники являются основной причиной [каскадных сбоев](https://en.wikipedia.org/wiki/Cascading_failure). Если возникает ошибка, функция должна вернуть ошибку и позволить вызывающей стороне решить, как её обработать.
ПлохоХорошо
```go func run(args []string) { if len(args) == 0 { panic("an argument is required") } // ... } func main() { run(os.Args[1:]) } ``` ```go 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. Исключением является инициализация программы: проблемы при запуске программы, которые должны её завершить, могут вызывать панику. ```go var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML")) ``` Даже в тестах предпочитайте `t.Fatal` или `t.FailNow` вместо паник, чтобы гарантировать, что тест будет помечен как неудачный.
ПлохоХорошо
```go // func TestFoo(t *testing.T) f, err := os.CreateTemp("", "test") if err != nil { panic("failed to set up test") } ``` ```go // 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](https://pkg.go.dev/sync/atomic) работают с базовыми типами (`int32`, `int64` и т.д.), поэтому легко забыть использовать атомарную операцию для чтения или изменения переменных. [go.uber.org/atomic](https://pkg.go.dev/go.uber.org/atomic) добавляет безопасность типов к этим операциям, скрывая базовый тип. Кроме того, он включает удобный тип `atomic.Bool`.
ПлохоХорошо
```go 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 // состояние гонки! } ``` ```go 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() } ```
### Избегайте изменяемых глобальных переменных Избегайте изменения глобальных переменных, отдавая предпочтение внедрению зависимостей. Это относится как к указателям на функции, так и к другим видам значений.
ПлохоХорошо
```go // sign.go var _timeNow = time.Now func sign(msg string) string { now := _timeNow() return signWithTime(msg, now) } ``` ```go // 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) } ```
```go // 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)) } ``` ```go // 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` в ваши конкретные реализации списков. Вместо этого напишите вручную только методы вашего конкретного списка, которые будут делегировать вызовы абстрактному списку. ```go type AbstractList struct {} // Add добавляет сущность в список. func (l *AbstractList) Add(e Entity) { // ... } // Remove удаляет сущность из списка. func (l *AbstractList) Remove(e Entity) { // ... } ```
ПлохоХорошо
```go // ConcreteList — это список сущностей. type ConcreteList struct { *AbstractList } ``` ```go // 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 позволяет [встраивание типов](https://go.dev/doc/effective_go#embedding) как компромисс между наследованием и композицией. Внешний тип получает неявные копии методов встроенного типа. Эти методы по умолчанию делегируют вызовы тому же методу встроенного экземпляра. Структура также получает поле с тем же именем, что и тип. Таким образом, если встроенный тип является публичным, поле также является публичным. Для сохранения обратной совместимости каждая будущая версия внешнего типа должна сохранять встроенный тип. Встраивание типа редко необходимо. Это удобство, которое помогает избежать написания утомительных делегирующих методов. Даже встраивание совместимого интерфейса `AbstractList` вместо структуры дало бы разработчику больше гибкости для изменений в будущем, но всё равно раскрыло бы деталь, что конкретные списки используют абстрактную реализацию.
ПлохоХорошо
```go // AbstractList — это обобщённая реализация для различных видов списков сущностей. type AbstractList interface { Add(Entity) Remove(Entity) } // ConcreteList — это список сущностей. type ConcreteList struct { AbstractList } ``` ```go // 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](https://go.dev/ref/spec) описывает несколько встроенных, [предобъявленных идентификаторов](https://go.dev/ref/spec#Predeclared_identifiers), которые не должны использоваться как имена в программах на Go. В зависимости от контекста повторное использование этих идентификаторов в качестве имён либо затеняет оригинал в текущей лексической области видимости (и любых вложенных областях), либо делает затронутый код запутанным. В лучшем случае компилятор пожалуется; в худшем — такой код может привести к скрытым, трудноуловимым ошибкам.
ПлохоХорошо
```go var error string // `error` затеняет встроенный идентификатор // или func handleErrorMessage(error string) { // `error` затеняет встроенный идентификатор } ``` ```go var errorMessage string // `error` ссылается на встроенный идентификатор // или func handleErrorMessage(msg string) { // `error` ссылается на встроенный идентификатор } ```
```go 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 } ``` ```go 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".
ПлохоХорошо
```go type Foo struct { // ... } var _defaultFoo Foo func init() { _defaultFoo = Foo{ // ... } } ``` ```go var _defaultFoo = Foo{ // ... } // или, лучше, для тестируемости: var _defaultFoo = defaultFoo() func defaultFoo() Foo { return Foo{ // ... } } ```
```go type Config struct { // ... } var _config Config func init() { // Плохо: зависит от текущей директории cwd, _ := os.Getwd() // Плохо: ввод-вывод raw, _ := os.ReadFile( path.Join(cwd, "config", "config.yaml"), ) yaml.Unmarshal(raw, &_config) } ``` ```go 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](https://cloud.google.com/functions/docs/bestpractices/tips#use_global_variables_to_reuse_objects_in_future_invocations) и другие формы детерминированного предварительного вычисления. ### Завершение программы в main Программы на Go используют [`os.Exit`](https://pkg.go.dev/os#Exit) или [`log.Fatal*`](https://pkg.go.dev/log#Fatal) для немедленного завершения. (Паника — нехороший способ завершения программ, пожалуйста, [не паникуйте](#не-паникуйте).) Вызывайте `os.Exit` или `log.Fatal*` **только в `main()`**. Все остальные функции должны возвращать ошибки для сигнализации о сбое.
ПлохоХорошо
```go 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) } ``` ```go 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()` и помещению всей ключевой бизнес-логики в отдельную, тестируемую функцию.
ПлохоХорошо
```go 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) } // ... } ``` ```go 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`. ```go func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` Вы можете изменить сигнатуру `run()`, чтобы она соответствовала вашим потребностям. Например, если ваша программа должна завершаться с определёнными кодами выхода при сбоях, `run()` может возвращать код выхода вместо ошибки. Это позволяет модульным тестам также напрямую проверять это поведение. ```go func main() { os.Exit(run(args)) } func run() (exitCode int) { // ... } ``` Более общо, обратите внимание, что функция `run()`, используемая в этих примерах, не является предписывающей. Существует гибкость в имени, сигнатуре и настройке функции `run()`. Среди прочего, вы можете: - принимать неразобранные аргументы командной строки (например, `run(os.Args[1:])`) - разбирать аргументы командной строки в `main()` и передавать их в `run` - использовать пользовательский тип ошибки для передачи кода выхода обратно в `main()` - помещать бизнес-логику на другой уровень абстракции, отличный от `package main` Эта рекомендация требует только, чтобы в вашем `main()` было единственное место, ответственное за фактическое завершение процесса. ### Используйте теги полей в структурах для сериализации Любое поле структуры, которое сериализуется в JSON, YAML или другие форматы, поддерживающие именование полей на основе тегов, должно быть аннотировано соответствующим тегом.
ПлохоХорошо
```go type Stock struct { Price int Name string } bytes, err := json.Marshal(Stock{ Price: 137, Name: "UBER", }) ``` ```go 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](https://pkg.go.dev/go.uber.org/goleak) для тестирования утечек горутин внутри пакетов, которые могут их создавать. В общем случае каждая горутина: - должна иметь предсказуемый момент, когда она перестанет выполняться; или - должен быть способ сигнализировать горутине, что ей следует остановиться В обоих случаях должен быть способ заблокировать выполнение и дождаться завершения горутины. Например:
ПлохоХорошо
```go go func() { for { flush() time.Sleep(delay) } }() ``` ```go 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`, чтобы дождаться завершения нескольких горутин. Делайте так, если нужно ждать несколько горутин. ```go var wg sync.WaitGroup for i := 0; i < N; i++ { wg.Go(...) } // Чтобы дождаться завершения всех: wg.Wait() ``` - Добавьте ещё один `chan struct{}`, который горутина закроет по завершении. Делайте так, если есть только одна горутина. ```go done := make(chan struct{}) go func() { defer close(done) // ... }() // Чтобы дождаться завершения горутины: <-done ``` #### Не используйте горутины в `init()` Функции `init()` не должны запускать горутины. См. также [Избегайте init()](#избегайте-init). Если пакету нужна фоновая горутина, он должен предоставлять объект, ответственный за управление временем жизни горутины. Этот объект должен предоставлять метод (`Close`, `Stop`, `Shutdown` и т.д.), который сигнализирует фоновой горутине об остановке и ждёт её завершения.
ПлохоХорошо
```go func init() { go doWork() } func doWork() { for { // ... } } ``` ```go 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`.
ПлохоХорошо
```go for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) } ``` ```go for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) } ```
```plain BenchmarkFmtSprint-4 143 ns/op 2 allocs/op ``` ```plain BenchmarkStrconv-4 64.2 ns/op 1 allocs/op ```
### Избегайте повторного преобразования строк в байты Не создавайте срезы байт из фиксированной строки повторно. Вместо этого выполните преобразование один раз и сохраните результат.
ПлохоХорошо
```go for i := 0; i < b.N; i++ { w.Write([]byte("Hello world")) } ``` ```go data := []byte("Hello world") for i := 0; i < b.N; i++ { w.Write(data) } ```
```plain BenchmarkBad-4 50000000 22.2 ns/op ``` ```plain BenchmarkGood-4 500000000 3.25 ns/op ```
### Предпочитайте указание ёмкости контейнеров По возможности указывайте ёмкость контейнеров, чтобы выделить память для контейнера заранее. Это минимизирует последующие выделения памяти (из-за копирования и изменения размера контейнера) при добавлении элементов. #### Указание подсказки ёмкости для карт По возможности предоставляйте подсказку ёмкости при инициализации карт с помощью `make()`. ```go make(map[T1]T2, hint) ``` Предоставление подсказки ёмкости для `make()` пытается правильно определить размер карты при инициализации, что уменьшает необходимость её роста и выделений памяти при добавлении элементов. Обратите внимание, что в отличие от срезов, подсказки ёмкости для карт не гарантируют полного, упреждающего выделения, а используются для приблизительного определения количества необходимых корзин хэш-карты. Следовательно, выделения памяти всё ещё могут происходить при добавлении элементов в карту, даже до указанной ёмкости.
ПлохоХорошо
```go m := make(map[string]os.FileInfo) files, _ := os.ReadDir("./files") for _, f := range files { m[f.Name()] = f } ``` ```go files, _ := os.ReadDir("./files") m := make(map[string]os.DirEntry, len(files)) for _, f := range files { m[f.Name()] = f } ```
`m` создаётся без подсказки размера; при присваивании может быть больше выделений памяти. `m` создаётся с подсказкой размера; при присваивании может быть меньше выделений памяти.
#### Указание ёмкости срезов По возможности предоставляйте подсказку ёмкости при инициализации срезов с помощью `make()`, особенно при использовании `append`. ```go make([]T, length, capacity) ``` В отличие от карт, ёмкость среза — это не подсказка: компилятор выделит достаточно памяти для ёмкости среза, предоставленной в `make()`, что означает, что последующие операции `append()` не будут приводить к выделениям памяти (пока длина среза не совпадёт с ёмкостью, после чего любые добавления потребуют изменения размера для хранения дополнительных элементов).
ПлохоХорошо
```go for n := 0; n < b.N; n++ { data := make([]int, 0) for k := 0; k < size; k++{ data = append(data, k) } } ``` ```go for n := 0; n < b.N; n++ { data := make([]int, 0, size) for k := 0; k < size; k++{ data = append(data, k) } } ```
```plain BenchmarkBad-4 100000000 2.48s ``` ```plain BenchmarkGood-4 100000000 0.21s ```
## Стиль ### Избегайте слишком длинных строк Избегайте строк кода, которые заставляют читателей прокручивать по горизонтали или слишком сильно поворачивать голову. Мы рекомендуем мягкое ограничение длины строки в **99 символов**. Авторы должны стараться переносить строки до достижения этого предела, но это не строгое ограничение. Коду разрешено превышать этот лимит. ### Будьте последовательны Некоторые рекомендации, изложенные в этом документе, можно оценить объективно; другие ситуативны, контекстны или субъективны. Прежде всего, **будьте последовательны**. Последовательный код легче поддерживать, легче осмыслить, требует меньше когнитивных усилий и легче переносить или обновлять по мере появления новых соглашений или исправления классов ошибок. И наоборот, наличие множества различных или конфликтующих стилей в одной кодовой базе создаёт накладные расходы на поддержку, неопределённость и когнитивный диссонанс, что напрямую может способствовать снижению скорости разработки, болезненным код-ревью и ошибкам. При применении этих рекомендаций к кодовой базе рекомендуется вносить изменения на уровне пакета (или выше): применение на уровне подпакета нарушает указанную выше проблему, внося несколько стилей в один код. ### Группируйте схожие объявления Go поддерживает группировку схожих объявлений.
ПлохоХорошо
```go import "a" import "b" ``` ```go import ( "a" "b" ) ```
Это также относится к константам, переменным и объявлениям типов.
ПлохоХорошо
```go const a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64 ``` ```go const ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 ) ```
Группируйте только связанные объявления. Не группируйте несвязанные объявления.
ПлохоХорошо
```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply EnvVar = "MY_ENV" ) ``` ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const EnvVar = "MY_ENV" ```
Группы не ограничены в том, где могут использоваться. Например, их можно использовать внутри функций.
ПлохоХорошо
```go func f() string { red := color.New(0xff0000) green := color.New(0x00ff00) blue := color.New(0x0000ff) // ... } ``` ```go func f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) // ... } ```
Исключение: Объявления переменных, особенно внутри функций, должны группироваться вместе, если они объявлены рядом с другими переменными. Делайте так для переменных, объявленных вместе, даже если они не связаны.
ПлохоХорошо
```go func (c *client) request() { caller := c.name format := "json" timeout := 5*time.Second var err error // ... } ``` ```go func (c *client) request() { var ( caller = c.name format = "json" timeout = 5*time.Second err error ) // ... } ```
### Порядок групп импорта Должно быть две группы импорта: - Стандартная библиотека - Все остальные Это группировка, применяемая по умолчанию в `goimports`.
ПлохоХорошо
```go import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" ) ``` ```go import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" ) ```
### Имена пакетов При именовании пакетов выбирайте имя, которое: - Состоит только из строчных букв. Без заглавных букв и подчёркиваний. - Не требует переименования с использованием именованных импортов в большинстве мест вызова. - Короткое и ёмкое. Помните, что имя полностью указывается в каждом месте вызова. - Не во множественном числе. Например, `net/url`, а не `net/urls`. - Не "common", "util", "shared" или "lib". Это плохие, неинформативные имена. См. также [Package Names](https://go.dev/blog/package-names) и [Style guideline for Go packages](https://rakyll.org/style-packages/). ### Имена функций Мы следуем соглашению сообщества Go об использовании [MixedCaps для имён функций](https://go.dev/doc/effective_go#mixed-caps). Исключение делается для тестовых функций, которые могут содержать подчёркивания для группировки связанных тестовых случаев, например, `TestMyFunction_WhatIsBeingTested`. ### Псевдонимы импорта Псевдонимы импорта должны использоваться, если имя пакета не совпадает с последним элементом пути импорта. ```go import ( "net/http" client "example.com/client-go" trace "example.com/trace/v2" ) ``` Во всех остальных сценариях псевдонимы импорта следует избегать, если нет прямого конфликта между импортами.
ПлохоХорошо
```go import ( "fmt" "os" runtimetrace "runtime/trace" nettrace "golang.net/x/trace" ) ``` ```go import ( "fmt" "os" "runtime/trace" nettrace "golang.net/x/trace" ) ```
### Группировка и порядок функций - Функции должны быть отсортированы в приблизительном порядке вызовов. - Функции в файле должны быть сгруппированы по получателю. Следовательно, экспортируемые функции должны появляться первыми в файле после определений `struct`, `const`, `var`. `newXYZ()`/`NewXYZ()` может появиться после определения типа, но до остальных методов получателя. Поскольку функции группируются по получателю, простые вспомогательные функции должны появляться ближе к концу файла.
ПлохоХорошо
```go 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{} } ``` ```go 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 {...} ```
### Уменьшайте вложенность Код должен уменьшать вложенность, где это возможно, обрабатывая случаи ошибок/особые условия первыми и возвращаясь рано или продолжая цикл. Уменьшайте количество кода, вложенного на несколько уровней.
ПлохоХорошо
```go 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) } } ``` ```go 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.
ПлохоХорошо
```go var a int if b { a = 100 } else { a = 10 } ``` ```go a := 10 if b { a = 100 } ```
### Объявления переменных верхнего уровня На верхнем уровне используйте стандартное ключевое слово `var`. Не указывайте тип, если он не отличается от типа выражения.
ПлохоХорошо
```go var _s string = F() func F() string { return "A" } ``` ```go var _s = F() // Поскольку F уже указывает, что возвращает строку, нам не нужно снова указывать тип. func F() string { return "A" } ```
Указывайте тип, если тип выражения не совпадает точно с желаемым типом. ```go type myError struct{} func (myError) Error() string { return "error" } func F() myError { return myError{} } var _e error = F() // F возвращает объект типа myError, но мы хотим error. ``` ### Префикс \_ для неэкспортируемых глобальных переменных Добавляйте префикс `_` к неэкспортируемым переменным и константам верхнего уровня, чтобы было ясно, что они являются глобальными символами, когда они используются. Обоснование: Переменные и константы верхнего уровня имеют область видимости пакета. Использование общего имени делает лёгким случайное использование неправильного значения в другом файле.
ПлохоХорошо
```go // foo.go const ( defaultPort = 8080 defaultUser = "user" ) // bar.go func Bar() { defaultPort := 9090 ... fmt.Println("Default port", defaultPort) // Мы не увидим ошибку компиляции, если первая строка Bar() будет удалена. } ``` ```go // foo.go const ( _defaultPort = 8080 _defaultUser = "user" ) ```
**Исключение**: Неэкспортируемые значения ошибок могут использовать префикс `err` без подчёркивания. См. [Именование ошибок](#именование-ошибок). ### Встраивание в структурах Встроенные типы должны находиться в начале списка полей структуры, и должна быть пустая строка, отделяющая встроенные поля от обычных полей.
ПлохоХорошо
```go type Client struct { version int http.Client } ``` ```go type Client struct { http.Client version int } ```
Встраивание должно обеспечивать ощутимую пользу, например, добавлять или расширять функциональность семантически-уместным способом. Оно должно делать это без каких-либо негативных последствий для пользователя (см. также: [Избегайте встраивания типов в публичные структуры](#избегайте-встраивания-типов-в-публичные-структуры)). Исключение: Мьютексы не должны встраиваться, даже в неэкспортируемые типы. См. также: [Нулевые значения мьютексов допустимы](#нулевые-значения-мьютексов-допустимы). Встраивание **НЕ должно**: - Быть чисто косметическим или ориентированным на удобство. - Усложнять создание или использование внешних типов. - Влиять на нулевые значения внешних типов. Если внешний тип имеет полезное нулевое значение, он должен сохранять его после встраивания внутреннего типа. - Раскрывать несвязанные функции или поля внешнего типа как побочный эффект встраивания внутреннего типа. - Раскрывать неэкспортируемые типы. - Влиять на семантику копирования внешних типов. - Менять API или семантику типов внешних типов. - Встраивать неканоническую форму внутреннего типа. - Раскрывать детали реализации внешнего типа. - Позволять пользователям наблюдать или контролировать внутренности типа. - Менять общее поведение внутренних функций через обёртывание таким образом, который может удивить пользователей. Проще говоря, встраивайте осознанно и преднамеренно. Хороший тест: "все ли эти экспортируемые внутренние методы/поля были бы добавлены напрямую к внешнему типу"; если ответ "некоторые" или "нет", не встраивайте внутренний тип — используйте поле.
ПлохоХорошо
```go type A struct { // Плохо: A.Lock() и A.Unlock() теперь доступны, не предоставляют функциональной пользы и позволяют пользователям контролировать детали внутренностей A. sync.Mutex } ``` ```go 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) } ```
```go type Book struct { // Плохо: указатель меняет полезность нулевого значения io.ReadWriter // другие поля } // позже var b Book b.Read(...) // panic: nil pointer b.String() // panic: nil pointer b.Write(...) // panic: nil pointer ``` ```go type Book struct { // Хорошо: имеет полезное нулевое значение bytes.Buffer // другие поля } // позже var b Book b.Read(...) // ok b.String() // ok b.Write(...) // ok ```
```go type Client struct { sync.Mutex sync.WaitGroup bytes.Buffer url.URL } ``` ```go type Client struct { mtx sync.Mutex wg sync.WaitGroup buf bytes.Buffer url url.URL } ```
### Объявление локальных переменных Короткое объявление переменных (`:=`) должно использоваться, если переменная явно устанавливается в некоторое значение.
ПлохоХорошо
```go var s = "foo" ``` ```go s := "foo" ```
Однако бывают случаи, когда значение по умолчанию понятнее при использовании ключевого слова `var`. [Объявление пустых срезов](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices), например.
ПлохоХорошо
```go func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } ``` ```go func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } ```
### nil — это валидный срез `nil` — это валидный срез длины 0. Это означает, что: - Не следует явно возвращать срез длины ноль. Возвращайте `nil` вместо этого.
ПлохоХорошо
```go if x == "" { return []int{} } ``` ```go if x == "" { return nil } ```
- Чтобы проверить, пуст ли срез, всегда используйте `len(s) == 0`. Не проверяйте на `nil`.
ПлохоХорошо
```go func isEmpty(s []string) bool { return s == nil } ``` ```go func isEmpty(s []string) bool { return len(s) == 0 } ```
- Нулевое значение (срез, объявленный с `var`) можно использовать сразу без `make()`.
ПлохоХорошо
```go nums := []int{} // или, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } ``` ```go var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } ```
Помните, что хотя nil-срез является валидным срезом, он не эквивалентен выделенному срезу длины 0 — один является nil, а другой нет — и они могут обрабатываться по-разному в разных ситуациях (например, при сериализации). ### Уменьшайте область видимости переменных По возможности уменьшайте область видимости переменных и констант. Не уменьшайте область видимости, если это противоречит [Уменьшению вложенности](#уменьшайте-вложенность).
ПлохоХорошо
```go err := os.WriteFile(name, data, 0644) if err != nil { return err } ``` ```go if err := os.WriteFile(name, data, 0644); err != nil { return err } ```
Если вам нужен результат вызова функции вне if, то не следует пытаться уменьшить область видимости.
ПлохоХорошо
```go 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 } ``` ```go data, err := os.ReadFile(name) if err != nil { return err } if err := cfg.Decode(data); err != nil { return err } fmt.Println(cfg) return nil ```
Константам не нужно быть глобальными, если они не используются в нескольких функциях или файлах или являются частью внешнего контракта пакета.
ПлохоХорошо
```go const ( _defaultPort = 8080 _defaultUser = "user" ) func Bar() { fmt.Println("Default port", _defaultPort) } ``` ```go func Bar() { const ( defaultPort = 8080 defaultUser = "user" ) fmt.Println("Default port", defaultPort) } ```
### Избегайте «голых» параметров «Голые» параметры в вызовах функций могут ухудшать читаемость. Добавляйте комментарии в стиле C (`/* ... */`) для имён параметров, когда их значение неочевидно.
ПлохоХорошо
```go // func printInfo(name string, isLocal, done bool) printInfo("foo", true, true) ``` ```go // func printInfo(name string, isLocal, done bool) printInfo("foo", true /* isLocal */, true /* done */) ```
Ещё лучше заменить «голые» типы `bool` пользовательскими типами для более читаемого и типобезопасного кода. Это позволяет в будущем иметь более двух состояний (true/false) для этого параметра. ```go 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 поддерживает [сырые строковые литералы](https://go.dev/ref/spec#raw_string_lit), которые могут занимать несколько строк и включать кавычки. Используйте их, чтобы избежать ручного экранирования строк, которое гораздо труднее читать.
ПлохоХорошо
```go wantError := "unknown name:\"test\"" ``` ```go wantError := `unknown error:"test"` ```
### Инициализация структур #### Используйте имена полей для инициализации структур Вы почти всегда должны указывать имена полей при инициализации структур. Теперь это обеспечивается [`go vet`](https://pkg.go.dev/cmd/vet).
ПлохоХорошо
```go k := User{"John", "Doe", true} ``` ```go k := User{ FirstName: "John", LastName: "Doe", Admin: true, } ```
Исключение: Имена полей _могут_ быть опущены в таблицах тестов, когда полей 3 или меньше. ```go tests := []struct{ op Operation want string }{ {Add, "add"}, {Subtract, "subtract"}, } ``` #### Опускайте поля с нулевыми значениями в структурах При инициализации структур с именами полей опускайте поля, имеющие нулевые значения, если они не предоставляют значимый контекст. В противном случае позвольте Go автоматически установить их в нулевые значения.
ПлохоХорошо
```go user := User{ FirstName: "John", LastName: "Doe", MiddleName: "", Admin: false, } ``` ```go user := User{ FirstName: "John", LastName: "Doe", } ```
Это помогает уменьшить шум для читателей, опуская значения, которые являются стандартными в данном контексте. Указываются только значимые значения. Включайте нулевые значения, когда имена полей предоставляют значимый контекст. Например, тестовые случаи в [Табличных тестах](#табличные-тесты) могут выиграть от указания имён полей, даже когда они имеют нулевые значения. ```go tests := []struct{ give string want int }{ {give: "0", want: 0}, // ... } ``` #### Используйте `var` для структур с нулевыми значениями Когда все поля структуры опущены в объявлении, используйте форму `var` для объявления структуры.
ПлохоХорошо
```go user := User{} ``` ```go var user User ```
Это отличает структуры с нулевыми значениями от тех, у которых есть ненулевые поля, аналогично различию, создаваемому для [инициализации карт](#инициализация-карт), и соответствует тому, как мы предпочитаем [объявлять пустые срезы](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices). #### Инициализация ссылок на структуры Используйте `&T{}` вместо `new(T)` при инициализации ссылок на структуры, чтобы это было согласовано с инициализацией структур.
ПлохоХорошо
```go sval := T{Name: "foo"} // несогласованно sptr := new(T) sptr.Name = "bar" ``` ```go sval := T{Name: "foo"} sptr := &T{Name: "bar"} ```
### Инициализация карт Предпочитайте `make(..)` для пустых карт и карт, заполняемых программно. Это делает инициализацию карт визуально отличной от объявления и позволяет легко добавить подсказку размера позже, если она доступна.
ПлохоХорошо
```go var ( // m1 безопасна для чтения и записи; // m2 вызовет панику при записи. m1 = map[T1]T2{} m2 map[T1]T2 ) ``` ```go var ( // m1 безопасна для чтения и записи; // m2 вызовет панику при записи. m1 = make(map[T1]T2) m2 map[T1]T2 ) ```
Объявление и инициализация визуально похожи. Объявление и инициализация визуально различны.
По возможности предоставляйте подсказку ёмкости при инициализации карт с помощью `make()`. См. [Указание подсказки ёмкости для карт](#указание-подсказки-ёмкости-для-карт) для получения дополнительной информации. С другой стороны, если карта содержит фиксированный список элементов, используйте литералы карт для её инициализации.
ПлохоХорошо
```go m := make(map[T1]T2, 3) m[k1] = v1 m[k2] = v2 m[k3] = v3 ``` ```go m := map[T1]T2{ k1: v1, k2: v2, k3: v3, } ```
Основное правило — использовать литералы карт при добавлении фиксированного набора элементов во время инициализации, в противном случае используйте `make` (и указывайте подсказку размера, если доступна). ### Строки формата вне Printf Если вы объявляете строки формата для функций в стиле `Printf` вне строкового литерала, сделайте их значениями `const`. Это помогает `go vet` выполнять статический анализ строки формата.
ПлохоХорошо
```go msg := "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2) ``` ```go const msg = "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2) ```
### Именование функций в стиле Printf Когда вы объявляете функцию в стиле `Printf`, убедитесь, что `go vet` может её обнаружить и проверить строку формата. Это означает, что следует использовать предопределённые имена функций в стиле `Printf`, если это возможно. `go vet` проверяет их по умолчанию. См. [Printf family](https://pkg.go.dev/cmd/vet#hdr-Printf_family) для получения дополнительной информации. Если использование предопределённых имён невозможно, заканчивайте выбранное вами имя на f: `Wrapf`, а не `Wrap`. `go vet` можно попросить проверять определённые имена в стиле `Printf`, но они должны заканчиваться на f. ```shell go vet -printfuncs=wrapf,statusf ``` См. также [go vet: Printf family check](https://kuzminva.wordpress.com/2017/11/07/go-vet-printf-family-check/). ## Паттерны ### Табличные тесты Табличные тесты с [подтестами](https://go.dev/blog/subtests) могут быть полезным паттерном для написания тестов, чтобы избежать дублирования кода, когда основная тестовая логика повторяется. Если тестируемую систему нужно проверить на соответствие _нескольким условиям_, где определённые части входных и выходных данных меняются, следует использовать табличные тесты, чтобы уменьшить избыточность и улучшить читаемость.
ПлохоХорошо
```go // 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) ``` ```go // 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`. ```go tests := []struct{ give string wantHost string wantPort string }{ // ... } for _, tt := range tests { // ... } ``` #### Избегайте излишней сложности в табличных тестах Табличные тесты могут быть трудны для чтения и поддержки, если подтесты содержат условные проверки или другую разветвлённую логику. Табличные тесты **НЕ ДОЛЖНЫ** использоваться всякий раз, когда внутри подтестов требуется сложная или условная логика (т.е. сложная логика внутри цикла `for`). Большие, сложные табличные тесты ухудшают читаемость и поддерживаемость, потому что читателям тестов может быть трудно отлаживать возникающие сбои тестов. Такие табличные тесты следует разделить либо на несколько таблиц тестов, либо на несколько отдельных функций `Test...`. К некоторым идеалам, к которым стоит стремиться, относятся: - Фокусировка на самой узкой единице поведения - Минимизация «глубины теста» и избегание условных проверок (см. ниже) - Обеспечение того, что все поля таблицы используются во всех тестах - Обеспечение того, что вся тестовая логика выполняется для всех случаев таблицы В этом контексте «глубина теста» означает «внутри данного теста, количество последовательных проверок, требующих выполнения предыдущих проверок» (аналогично цикломатической сложности). Наличие «более мелких» тестов означает меньше связей между проверками и, что более важно, что эти проверки по умолчанию менее вероятно будут условными. Конкретно, табличные тесты могут стать запутанными и трудными для чтения, если они используют несколько ветвящихся путей (например, `shouldError`, `expectCall` и т.д.), используют много операторов `if` для специфичных ожиданий моков (например, `shouldCallFoo`) или размещают функции внутри таблицы (например, `setupMocks func(*FooMock)`). Однако при тестировании поведения, которое меняется только в зависимости от изменённых входных данных, может быть предпочтительнее группировать схожие случаи вместе в табличном тесте, чтобы лучше иллюстрировать, как поведение меняется при всех входных данных, а не разделять иначе сопоставимые единицы на отдельные тесты и делать их более трудными для сравнения и противопоставления. Если тело теста короткое и простое, допустимо иметь единственный ветвящийся путь для случаев успеха и неудачи с полем таблицы типа `shouldErr` для указания ожиданий ошибки.
ПлохоХорошо
```go 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) }) } } ``` ```go 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") } ```
Эта сложность делает тест более трудным для изменения, понимания и доказательства его корректности. Хотя строгих правил нет, читаемость и поддерживаемость всегда должны быть главными при выборе между табличными тестами и отдельными тестами для множественных входов/выходов системы. #### Параллельные тесты Параллельные тесты, как и некоторые специализированные циклы (например, те, что создают горутины или захватывают ссылки как часть тела цикла), должны заботиться о явном присваивании переменных цикла внутри области видимости цикла, чтобы гарантировать, что они содержат ожидаемые значения. ```go 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, которые, как вы предвидите, могут потребовать расширения, особенно если у вас уже есть три или более аргументов в этих функциях.
ПлохоХорошо
```go // package db func Open( addr string, cache bool, logger *zap.Logger ) (*Connection, error) { // ... } ``` ```go // 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 всегда должны предоставляться, даже если пользователь хочет использовать значения по умолчанию. ```go 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) ``` Опции предоставляются только при необходимости. ```go 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`. ```go 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`, что позволяет создавать удобочитаемые строковые представления опций. См. также, - [Self-referential functions and the design of options](https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html) - [Functional options for friendly APIs](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) ## Линтинг Более важно, чем любой «благословленный» набор линтеров, — линтить последовательно по всей кодовой базе. Мы рекомендуем использовать следующие линтеры как минимум, потому что считаем, что они помогают выявить наиболее распространённые проблемы, а также устанавливают высокую планку качества кода, не будучи излишне предписывающими: - [errcheck](https://github.com/kisielk/errcheck) для обеспечения обработки ошибок - [goimports](https://pkg.go.dev/golang.org/x/tools/cmd/goimports) для форматирования кода и управления импортами - [golint](https://github.com/golang/lint) для указания на распространённые стилевые ошибки - [govet](https://pkg.go.dev/cmd/vet) для анализа кода на распространённые ошибки - [staticcheck](https://staticcheck.dev) для выполнения различных проверок статического анализа ### Запускатели линтеров Мы рекомендуем [golangci-lint](https://github.com/golangci/golangci-lint) в качестве основного запускателя линтеров для кода на Go, во многом благодаря его производительности в больших кодовых базах и возможности настраивать и использовать многие канонические линтеры одновременно. Этот репозиторий содержит пример [.golangci.yml](https://github.com/uber-go/guide/blob/master/.golangci.yml) файла конфигурации с рекомендуемыми линтерами и настройками. golangci-lint имеет [различные линтеры](https://golangci-lint.run/usage/linters/), доступные для использования. Вышеуказанные линтеры рекомендуются в качестве базового набора, и мы поощряем команды добавлять любые дополнительные линтеры, которые имеют смысл для их проектов.