From 49458f5ffd5a48c465117ec27f6437683f75acc1 Mon Sep 17 00:00:00 2001 From: Alexander Neonxp Kiryukhin Date: Sat, 31 Jan 2026 20:38:50 +0300 Subject: initial --- content/pages/gostyleguide/uber/index.md | 4140 ++++++++++++++++++++++++++++++ 1 file changed, 4140 insertions(+) create mode 100644 content/pages/gostyleguide/uber/index.md (limited to 'content/pages/gostyleguide/uber/index.md') diff --git a/content/pages/gostyleguide/uber/index.md b/content/pages/gostyleguide/uber/index.md new file mode 100644 index 0000000..a8e74fd --- /dev/null +++ b/content/pages/gostyleguide/uber/index.md @@ -0,0 +1,4140 @@ +--- +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/), доступные для использования. +Вышеуказанные линтеры рекомендуются в качестве базового набора, и мы поощряем +команды добавлять любые дополнительные линтеры, которые имеют смысл для их +проектов. -- cgit v1.2.3