summaryrefslogtreecommitdiff
path: root/content/pages/gostyleguide/uber
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--content/pages/gostyleguide/uber/index.md4140
1 files changed, 4140 insertions, 0 deletions
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
+
+<!--more-->
+
+- [Введение](#введение)
+- [Рекомендации](#рекомендации)
+ - [Указатели на интерфейсы](#указатели-на-интерфейсы)
+ - [Проверка соответствия интерфейсу](#проверка-соответствия-интерфейсу)
+ - [Получатели и интерфейсы](#получатели-и-интерфейсы)
+ - [Нулевые значения мьютексов
+ допустимы](#нулевые-значения-мьютексов-допустимы)
+ - [Копируйте срезы и карты на границах](#копируйте-срезы-и-карты-на-границах)
+ - [Используйте `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-контракта
+- Экспортируемые или неэкспортируемые типы, которые являются частью коллекции
+ типов, реализующих один и тот же интерфейс
+- Другие случаи, когда нарушение интерфейса приведёт к поломке пользователей
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+type Handler struct {
+ // ...
+}
+
+
+
+func (h *Handler) ServeHTTP(
+ w http.ResponseWriter,
+ r *http.Request,
+) {
+ ...
+}
+```
+
+</td><td>
+
+```go
+type Handler struct {
+ // ...
+}
+
+var _ http.Handler = (*Handler)(nil)
+
+func (h *Handler) ServeHTTP(
+ w http.ResponseWriter,
+ r *http.Request,
+) {
+ // ...
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Выражение `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` является допустимым, поэтому
+почти никогда не нужен указатель на мьютекс.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+mu := new(sync.Mutex)
+mu.Lock()
+```
+
+</td><td>
+
+```go
+var mu sync.Mutex
+mu.Lock()
+```
+
+</td></tr>
+</tbody></table>
+
+Если вы используете структуру по указателю, то мьютекс должен быть не
+указателем, а полем в ней. Не встраивайте мьютекс в структуру, даже если
+структура не экспортируется.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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]
+}
+```
+
+</td><td>
+
+```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]
+}
+```
+
+</td></tr>
+
+<tr><td>
+
+Поле `Mutex`, а также методы `Lock` и `Unlock` непреднамеренно становятся частью
+публичного API `SMap`.
+
+</td><td>
+
+Мьютекс и его методы являются деталями реализации `SMap`, скрытыми от его
+вызывающих сторон.
+
+</td></tr>
+</tbody></table>
+
+### Копируйте срезы и карты на границах
+
+Срезы и карты содержат указатели на базовые данные, поэтому будьте осторожны в
+ситуациях, когда их нужно скопировать.
+
+#### Получение срезов и карт
+
+Помните, что пользователи могут изменить карту или срез, которые вы получили в
+качестве аргумента, если сохраните ссылку на них.
+
+<table>
+<thead><tr><th>Плохо</th> <th>Хорошо</th></tr></thead>
+<tbody>
+<tr>
+<td>
+
+```go
+func (d *Driver) SetTrips(trips []Trip) {
+ d.trips = trips
+}
+
+trips := ...
+d1.SetTrips(trips)
+
+// Вы хотели изменить d1.trips?
+trips[0] = ...
+```
+
+</td>
+<td>
+
+```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] = ...
+```
+
+</td>
+</tr>
+
+</tbody>
+</table>
+
+#### Возврат срезов и карт
+
+Аналогично, будьте осторожны с модификациями карт или срезов, раскрывающих
+внутреннее состояние.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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()
+```
+
+</td><td>
+
+```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()
+```
+
+</td></tr>
+</tbody></table>
+
+### Используйте `defer` для очистки
+
+Используйте `defer` для очистки ресурсов, таких как файлы и блокировки.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+p.Lock()
+if p.count < 10 {
+ p.Unlock()
+ return p.count
+}
+
+p.count++
+newCount := p.count
+p.Unlock()
+
+return newCount
+
+// легко пропустить разблокировки из-за множественных возвратов
+```
+
+</td><td>
+
+```go
+p.Lock()
+defer p.Unlock()
+
+if p.count < 10 {
+ return p.count
+}
+
+p.count++
+return p.count
+
+// более читаемо
+```
+
+</td></tr>
+</tbody></table>
+
+`defer` имеет крайне малые накладные расходы, и его следует избегать только если
+вы можете доказать, что время выполнения вашей функции измеряется в
+наносекундах. Выигрыш в читаемости от использования `defer` стоит той мизерной
+стоимости, которую он вносит. Это особенно верно для больших методов, где
+присутствуют не только простые операции доступа к памяти, а другие вычисления
+более значимы, чем `defer`.
+
+### Размер канала — один или ноль
+
+Каналы обычно должны иметь размер один или быть небуферизированными. По
+умолчанию каналы небуферизированы и имеют размер ноль. Любой другой размер
+должен подвергаться тщательному анализу. Подумайте, как определяется размер, что
+предотвращает заполнение канала под нагрузкой и блокировку писателей, и что
+происходит, когда это случается.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// Должно хватить на всех!
+c := make(chan int, 64)
+```
+
+</td><td>
+
+```go
+// Размер один
+c := make(chan int, 1) // или
+// Небуферизированный канал, размер ноль
+c := make(chan int)
+```
+
+</td></tr>
+</tbody></table>
+
+### Начинайте перечисления с единицы
+
+Стандартный способ введения перечислений в Go — объявление пользовательского
+типа и группы `const` с `iota`. Поскольку переменные имеют значение по умолчанию
+0, обычно следует начинать перечисления с ненулевого значения.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+type Operation int
+
+const (
+ Add Operation = iota
+ Subtract
+ Multiply
+)
+
+// Add=0, Subtract=1, Multiply=2
+```
+
+</td><td>
+
+```go
+type Operation int
+
+const (
+ Add Operation = iota + 1
+ Subtract
+ Multiply
+)
+
+// Add=1, Subtract=2, Multiply=3
+```
+
+</td></tr>
+</tbody></table>
+
+Бывают случаи, когда использование нулевого значения имеет смысл, например,
+когда случай с нулевым значением является желаемым поведением по умолчанию.
+
+```go
+type LogOutput int
+
+const (
+ LogToStdout LogOutput = iota
+ LogToFile
+ LogToRemote
+)
+
+// LogToStdout=0, LogToFile=1, LogToRemote=2
+```
+
+<!-- TODO: раздел о String методах для перечислений -->
+
+### Используйте `"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` для сравнения, добавления или вычитания времени.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+func isActive(now, start, stop int) bool {
+ return start <= now && now < stop
+}
+```
+
+</td><td>
+
+```go
+func isActive(now, start, stop time.Time) bool {
+ return (start.Before(now) || start.Equal(now)) && now.Before(stop)
+}
+```
+
+</td></tr>
+</tbody></table>
+
+#### Используйте `time.Duration` для промежутков времени
+
+Используйте [`time.Duration`](https://pkg.go.dev/time#Duration) при работе с
+промежутками времени.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+func poll(delay int) {
+ for {
+ // ...
+ time.Sleep(time.Duration(delay) * time.Millisecond)
+ }
+}
+
+poll(10) // это секунды или миллисекунды?
+```
+
+</td><td>
+
+```go
+func poll(delay time.Duration) {
+ for {
+ // ...
+ time.Sleep(delay)
+ }
+}
+
+poll(10*time.Second)
+```
+
+</td></tr>
+</tbody></table>
+
+Возвращаясь к примеру добавления 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`, единица
+измерения включается в имя поля.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// {"interval": 2}
+type Config struct {
+ Interval int `json:"interval"`
+}
+```
+
+</td><td>
+
+```go
+// {"intervalMillis": 2000}
+type Config struct {
+ IntervalMillis int `json:"intervalMillis"`
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Если невозможно использовать `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`, если вызывающей стороне нужно
+сопоставить и обработать эту ошибку.
+
+<table>
+<thead><tr><th>Без сопоставления ошибок</th><th>С сопоставлением ошибок</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// package foo
+
+func Open() error {
+ return errors.New("could not open")
+}
+
+// package bar
+
+if err := foo.Open(); err != nil {
+ // Не можем обработать ошибку.
+ panic("unknown error")
+}
+```
+
+</td><td>
+
+```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")
+ }
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Для ошибки с динамической строкой используйте
+[`fmt.Errorf`](https://pkg.go.dev/fmt#Errorf), если вызывающей стороне не нужно
+её сопоставлять, и пользовательский `error`, если нужно.
+
+<table>
+<thead><tr><th>Без сопоставления ошибок</th><th>С сопоставлением ошибок</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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")
+}
+```
+
+</td><td>
+
+```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, &notFound) {
+ // обработать ошибку
+ } else {
+ panic("unknown error")
+ }
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Обратите внимание, что если вы экспортируете переменные или типы ошибок из
+пакета, они станут частью публичного API пакета.
+
+#### Обёртывание ошибок
+
+Есть три основных варианта распространения ошибок при неудачном вызове:
+
+- вернуть исходную ошибку как есть
+- добавить контекст с помощью `fmt.Errorf` и глагола `%w`
+- добавить контекст с помощью `fmt.Errorf` и глагола `%v`
+
+Возвращайте исходную ошибку как есть, если нечего добавить к контексту. Это
+сохраняет исходный тип и сообщение ошибки. Это хорошо подходит для случаев,
+когда базовое сообщение об ошибке содержит достаточно информации для
+отслеживания её происхождения.
+
+В противном случае добавляйте контекст к сообщению об ошибке, где это возможно,
+чтобы вместо расплывчатой ошибки вроде "connection refused" вы получали более
+полезные ошибки, такие как "call service foo: connection refused".
+
+Используйте `fmt.Errorf` для добавления контекста к вашим ошибкам, выбирая между
+глаголами `%w` или `%v` в зависимости от того, должна ли вызывающая сторона
+иметь возможность сопоставить и извлечь базовую причину.
+
+- Используйте `%w`, если вызывающая сторона должна иметь доступ к базовой
+ ошибке. Это хороший вариант по умолчанию для большинства обёрнутых ошибок, но
+ имейте в виду, что вызывающие стороны могут начать полагаться на это
+ поведение. Поэтому для случаев, когда обёрнутая ошибка является известной
+ `var` или типом, документируйте и тестируйте это как часть контракта вашей
+ функции.
+- Используйте `%v`, чтобы скрыть базовую ошибку. Вызывающие стороны не смогут её
+ сопоставить, но вы сможете переключиться на `%w` в будущем, если потребуется.
+
+При добавлении контекста к возвращаемым ошибкам сохраняйте контекст кратким,
+избегая фраз типа "failed to", которые констатируют очевидное и накапливаются по
+мере всплытия ошибки по стеку:
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+s, err := store.New()
+if err != nil {
+ return fmt.Errorf(
+ "failed to create new store: %w", err)
+}
+```
+
+</td><td>
+
+```go
+s, err := store.New()
+if err != nil {
+ return fmt.Errorf(
+ "new store: %w", err)
+}
+```
+
+</td></tr><tr><td>
+
+```plain
+failed to x: failed to y: failed to create new store: the error
+```
+
+</td><td>
+
+```plain
+x: y: new store: the error
+```
+
+</td></tr>
+</tbody></table>
+
+Однако, как только ошибка отправляется в другую систему, должно быть понятно,
+что сообщение является ошибкой (например, тег `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
+- если ошибка представляет собой отказ в предметной области, вернуть чётко
+ определённую ошибку
+- вернуть ошибку, либо [обёрнутую](#обёртывание-ошибок), либо как есть
+
+Независимо от того, как вызывающая сторона обрабатывает ошибку, обычно она
+должна обрабатывать каждую ошибку только один раз. Вызывающая сторона не должна,
+например, логировать ошибку, а затем возвращать её, потому что _её_ вызывающие
+стороны тоже могут обработать ошибку.
+
+Например, рассмотрим следующие случаи:
+
+<table>
+<thead><tr><th>Описание</th><th>Код</th></tr></thead>
+<tbody>
+<tr><td>
+
+**Плохо**: Логировать ошибку и возвращать её
+
+Вызывающие стороны выше по стеку, вероятно, предпримут аналогичные действия с
+ошибкой. Это приведёт к большому количеству шума в логах приложения при малой
+пользе.
+
+</td><td>
+
+```go
+u, err := getUser(id)
+if err != nil {
+ // ПЛОХО: см. описание
+ log.Printf("Could not get user %q: %v", id, err)
+ return err
+}
+```
+
+</td></tr>
+<tr><td>
+
+**Хорошо**: Обернуть ошибку и вернуть её
+
+Вызывающие стороны выше по стеку обработают ошибку. Использование `%w`
+гарантирует, что они смогут сопоставить ошибку с `errors.Is` или `errors.As`,
+если это уместно.
+
+</td><td>
+
+```go
+u, err := getUser(id)
+if err != nil {
+ return fmt.Errorf("get user %q: %w", id, err)
+}
+```
+
+</td></tr>
+<tr><td>
+
+**Хорошо**: Логировать ошибку и выполнить graceful degradation
+
+Если операция не является строго необходимой, мы можем обеспечить
+деградировавший, но работающий опыт, восстановившись после ошибки.
+
+</td><td>
+
+```go
+if err := emitMetrics(); err != nil {
+ // Неудача при записи метрик не должна ломать приложение.
+ log.Printf("Could not emit metrics: %v", err)
+}
+
+```
+
+</td></tr>
+<tr><td>
+
+**Хорошо**: Сопоставить ошибку и выполнить graceful degradation
+
+Если вызываемая функция определяет конкретную ошибку в своём контракте и сбой
+является восстанавливаемым, сопоставьте этот случай ошибки и выполните graceful
+degradation. Для всех остальных случаев оберните ошибку и верните её.
+
+Вызывающие стороны выше по стеку обработают другие ошибки.
+
+</td><td>
+
+```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)
+ }
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Обрабатывайте сбои утверждения типа
+
+Форма утверждения типа с одним возвращаемым значением вызовет панику при
+неверном типе. Поэтому всегда используйте идиому "comma ok".
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+t := i.(string)
+```
+
+</td><td>
+
+```go
+t, ok := i.(string)
+if !ok {
+ // обработать ошибку корректно
+}
+```
+
+</td></tr>
+</tbody></table>
+
+<!-- TODO: Есть несколько ситуаций, где форма с одним присваиванием допустима. -->
+
+### Не паникуйте
+
+Код, работающий в production, должен избегать паник. Паники являются основной
+причиной [каскадных сбоев](https://en.wikipedia.org/wiki/Cascading_failure).
+Если возникает ошибка, функция должна вернуть ошибку и позволить вызывающей
+стороне решить, как её обработать.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+func run(args []string) {
+ if len(args) == 0 {
+ panic("an argument is required")
+ }
+ // ...
+}
+
+func main() {
+ run(os.Args[1:])
+}
+```
+
+</td><td>
+
+```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)
+ }
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Panic/recover — это не стратегия обработки ошибок. Программа должна паниковать
+только при возникновении чего-то невосстановимого, например, разыменовании nil.
+Исключением является инициализация программы: проблемы при запуске программы,
+которые должны её завершить, могут вызывать панику.
+
+```go
+var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
+```
+
+Даже в тестах предпочитайте `t.Fatal` или `t.FailNow` вместо паник, чтобы
+гарантировать, что тест будет помечен как неудачный.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// func TestFoo(t *testing.T)
+
+f, err := os.CreateTemp("", "test")
+if err != nil {
+ panic("failed to set up test")
+}
+```
+
+</td><td>
+
+```go
+// func TestFoo(t *testing.T)
+
+f, err := os.CreateTemp("", "test")
+if err != nil {
+ t.Fatal("failed to set up test")
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Используйте 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`.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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 // состояние гонки!
+}
+```
+
+</td><td>
+
+```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()
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Избегайте изменяемых глобальных переменных
+
+Избегайте изменения глобальных переменных, отдавая предпочтение внедрению
+зависимостей. Это относится как к указателям на функции, так и к другим видам
+значений.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// sign.go
+
+var _timeNow = time.Now
+
+func sign(msg string) string {
+ now := _timeNow()
+ return signWithTime(msg, now)
+}
+```
+
+</td><td>
+
+```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)
+}
+```
+
+</td></tr>
+<tr><td>
+
+```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))
+}
+```
+
+</td><td>
+
+```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))
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Избегайте встраивания типов в публичные структуры
+
+Такие встроенные типы раскрывают детали реализации, препятствуют эволюции типов
+и затрудняют чтение документации.
+
+Предположим, вы реализовали различные типы списков, используя общий
+`AbstractList`. Избегайте встраивания `AbstractList` в ваши конкретные
+реализации списков. Вместо этого напишите вручную только методы вашего
+конкретного списка, которые будут делегировать вызовы абстрактному списку.
+
+```go
+type AbstractList struct {}
+
+// Add добавляет сущность в список.
+func (l *AbstractList) Add(e Entity) {
+ // ...
+}
+
+// Remove удаляет сущность из списка.
+func (l *AbstractList) Remove(e Entity) {
+ // ...
+}
+```
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// ConcreteList — это список сущностей.
+type ConcreteList struct {
+ *AbstractList
+}
+```
+
+</td><td>
+
+```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)
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Go позволяет [встраивание типов](https://go.dev/doc/effective_go#embedding) как
+компромисс между наследованием и композицией. Внешний тип получает неявные копии
+методов встроенного типа. Эти методы по умолчанию делегируют вызовы тому же
+методу встроенного экземпляра.
+
+Структура также получает поле с тем же именем, что и тип. Таким образом, если
+встроенный тип является публичным, поле также является публичным. Для сохранения
+обратной совместимости каждая будущая версия внешнего типа должна сохранять
+встроенный тип.
+
+Встраивание типа редко необходимо. Это удобство, которое помогает избежать
+написания утомительных делегирующих методов.
+
+Даже встраивание совместимого интерфейса `AbstractList` вместо структуры дало бы
+разработчику больше гибкости для изменений в будущем, но всё равно раскрыло бы
+деталь, что конкретные списки используют абстрактную реализацию.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// AbstractList — это обобщённая реализация для различных видов списков сущностей.
+type AbstractList interface {
+ Add(Entity)
+ Remove(Entity)
+}
+
+// ConcreteList — это список сущностей.
+type ConcreteList struct {
+ AbstractList
+}
+```
+
+</td><td>
+
+```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)
+}
+```
+
+</td></tr>
+</tbody></table>
+
+И в случае со встроенной структурой, и со встроенным интерфейсом встроенный тип
+накладывает ограничения на эволюцию типа.
+
+- Добавление методов во встроенный интерфейс — это breaking change.
+- Удаление методов из встроенной структуры — это breaking change.
+- Удаление встроенного типа — это breaking change.
+- Замена встроенного типа, даже на альтернативу, удовлетворяющую тому же
+ интерфейсу, — это breaking change.
+
+Хотя написание этих делегирующих методов утомительно, дополнительные усилия
+скрывают деталь реализации, оставляют больше возможностей для изменений, а также
+устраняют косвенность при обнаружении полного интерфейса `List` в документации.
+
+### Избегайте использования встроенных имён
+
+[Спецификация языка Go](https://go.dev/ref/spec) описывает несколько встроенных,
+[предобъявленных
+идентификаторов](https://go.dev/ref/spec#Predeclared_identifiers), которые не
+должны использоваться как имена в программах на Go.
+
+В зависимости от контекста повторное использование этих идентификаторов в
+качестве имён либо затеняет оригинал в текущей лексической области видимости (и
+любых вложенных областях), либо делает затронутый код запутанным. В лучшем
+случае компилятор пожалуется; в худшем — такой код может привести к скрытым,
+трудноуловимым ошибкам.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+var error string
+// `error` затеняет встроенный идентификатор
+
+// или
+
+func handleErrorMessage(error string) {
+ // `error` затеняет встроенный идентификатор
+}
+```
+
+</td><td>
+
+```go
+var errorMessage string
+// `error` ссылается на встроенный идентификатор
+
+// или
+
+func handleErrorMessage(msg string) {
+ // `error` ссылается на встроенный идентификатор
+}
+```
+
+</td></tr>
+<tr><td>
+
+```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
+}
+```
+
+</td><td>
+
+```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
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Обратите внимание, что компилятор не будет генерировать ошибки при использовании
+предобъявленных идентификаторов, но такие инструменты, как `go vet`, должны
+корректно указывать на эти и другие случаи затенения.
+
+### Избегайте `init()`
+
+Избегайте `init()` там, где это возможно. Когда `init()` неизбежен или
+желателен, код должен пытаться:
+
+1. Быть полностью детерминированным, независимо от среды программы или вызова.
+2. Избегать зависимости от порядка или побочных эффектов других функций
+ `init()`. Хотя порядок `init()` хорошо известен, код может меняться, и
+ поэтому зависимости между функциями `init()` могут сделать код хрупким и
+ подверженным ошибкам.
+3. Избегать доступа или манипуляции глобальным состоянием или состоянием
+ окружения, таким как информация о машине, переменные окружения, рабочая
+ директория, аргументы/вводы программы и т.д.
+4. Избегать ввода-вывода, включая файловую систему, сеть и системные вызовы.
+
+Код, который не может удовлетворить этим требованиям, вероятно, должен быть
+вспомогательной функцией, вызываемой как часть `main()` (или в другом месте
+жизненного цикла программы), или быть написан как часть самого `main()`. В
+частности, библиотеки, предназначенные для использования другими программами,
+должны особенно тщательно следить за полной детерминированностью и не выполнять
+"init magic".
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+type Foo struct {
+ // ...
+}
+
+var _defaultFoo Foo
+
+func init() {
+ _defaultFoo = Foo{
+ // ...
+ }
+}
+```
+
+</td><td>
+
+```go
+var _defaultFoo = Foo{
+ // ...
+}
+
+// или, лучше, для тестируемости:
+
+var _defaultFoo = defaultFoo()
+
+func defaultFoo() Foo {
+ return Foo{
+ // ...
+ }
+}
+```
+
+</td></tr>
+<tr><td>
+
+```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)
+}
+```
+
+</td><td>
+
+```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
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Учитывая вышесказанное, некоторые ситуации, в которых `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()`**. Все остальные
+функции должны возвращать ошибки для сигнализации о сбое.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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)
+}
+```
+
+</td><td>
+
+```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
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Обоснование: Программы с несколькими функциями, которые завершают выполнение,
+создают несколько проблем:
+
+- Неочевидный поток управления: любая функция может завершить программу, поэтому
+ становится трудно рассуждать о потоке управления.
+- Сложность тестирования: функция, завершающая программу, также завершит тест,
+ который её вызывает. Это делает функцию трудной для тестирования и создаёт
+ риск пропуска других тестов, которые ещё не были запущены `go test`.
+- Пропущенная очистка: когда функция завершает программу, она пропускает вызовы
+ функций, поставленные в очередь с операторами `defer`. Это добавляет риск
+ пропуска важных задач очистки.
+
+#### Завершайте программу один раз
+
+По возможности старайтесь вызывать `os.Exit` или `log.Fatal` **не более одного
+раза** в вашем `main()`. Если есть несколько сценариев ошибок, которые
+останавливают выполнение программы, поместите эту логику в отдельную функцию и
+возвращайте из неё ошибки.
+
+Это приводит к сокращению функции `main()` и помещению всей ключевой
+бизнес-логики в отдельную, тестируемую функцию.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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)
+ }
+
+ // ...
+}
+```
+
+</td><td>
+
+```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
+ }
+
+ // ...
+}
+```
+
+</td></tr>
+</tbody></table>
+
+В примере выше используется `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 или другие форматы,
+поддерживающие именование полей на основе тегов, должно быть аннотировано
+соответствующим тегом.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+type Stock struct {
+ Price int
+ Name string
+}
+
+bytes, err := json.Marshal(Stock{
+ Price: 137,
+ Name: "UBER",
+})
+```
+
+</td><td>
+
+```go
+type Stock struct {
+ Price int `json:"price"`
+ Name string `json:"name"`
+ // Безопасно переименовать Name в Symbol.
+}
+
+bytes, err := json.Marshal(Stock{
+ Price: 137,
+ Name: "UBER",
+})
+```
+
+</td></tr>
+</tbody></table>
+
+Обоснование: Сериализованная форма структуры — это контракт между различными
+системами. Изменения в структуре сериализованной формы — включая имена полей —
+нарушают этот контракт. Указание имён полей внутри тегов делает контракт явным и
+защищает от случайного его нарушения при рефакторинге или переименовании полей.
+
+### Не запускайте горутины по принципу «запустил и забыл»
+
+Горутины легковесны, но не бесплатны: как минимум, они требуют памяти для своего
+стека и процессорного времени для планирования. Хотя эти затраты малы для
+типичного использования горутин, они могут вызвать значительные проблемы с
+производительностью, если горутины создаются в больших количествах без
+контролируемого времени жизни. Горутины с неуправляемым временем жизни также
+могут вызывать другие проблемы, например, мешать сборке мусора для
+неиспользуемых объектов и удерживать ресурсы, которые в противном случае больше
+не используются.
+
+Поэтому не допускайте утечек горутин в production коде. Используйте
+[go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) для тестирования
+утечек горутин внутри пакетов, которые могут их создавать.
+
+В общем случае каждая горутина:
+
+- должна иметь предсказуемый момент, когда она перестанет выполняться; или
+- должен быть способ сигнализировать горутине, что ей следует остановиться
+
+В обоих случаях должен быть способ заблокировать выполнение и дождаться
+завершения горутины.
+
+Например:
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+go func() {
+ for {
+ flush()
+ time.Sleep(delay)
+ }
+}()
+```
+
+</td><td>
+
+```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 // и ждать её завершения
+```
+
+</td></tr>
+<tr><td>
+
+Нет способа остановить эту горутину. Она будет работать, пока приложение не
+завершится.
+
+</td><td>
+
+Эту горутину можно остановить с помощью `close(stop)`, и мы можем дождаться её
+завершения с помощью `<-done`.
+
+</td></tr>
+</tbody></table>
+
+#### Дожидайтесь завершения горутин
+
+Для горутины, созданной системой, должен быть способ дождаться её завершения.
+Есть два популярных способа сделать это:
+
+- Используйте `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` и т.д.), который сигнализирует
+фоновой горутине об остановке и ждёт её завершения.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+func init() {
+ go doWork()
+}
+
+func doWork() {
+ for {
+ // ...
+ }
+}
+```
+
+</td><td>
+
+```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
+}
+```
+
+</td></tr>
+<tr><td>
+
+Создаёт фоновую горутину безусловно при экспорте этого пакета пользователем.
+Пользователь не имеет контроля над горутиной или способа её остановки.
+
+</td><td>
+
+Создаёт воркера только если пользователь его запрашивает. Предоставляет способ
+остановки воркера, чтобы пользователь мог освободить используемые им ресурсы.
+
+Обратите внимание, что следует использовать `WaitGroup`, если воркер управляет
+несколькими горутинами. См. [Дожидайтесь завершения
+горутин](#дожидайтесь-завершения-горутин).
+
+</td></tr>
+</tbody></table>
+
+## Производительность
+
+Рекомендации, специфичные для производительности, применяются только к «горячему
+пути» (hot path).
+
+### Предпочитайте strconv вместо fmt
+
+При преобразовании примитивов в строки и обратно `strconv` быстрее, чем `fmt`.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+for i := 0; i < b.N; i++ {
+ s := fmt.Sprint(rand.Int())
+}
+```
+
+</td><td>
+
+```go
+for i := 0; i < b.N; i++ {
+ s := strconv.Itoa(rand.Int())
+}
+```
+
+</td></tr>
+<tr><td>
+
+```plain
+BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
+```
+
+</td><td>
+
+```plain
+BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
+```
+
+</td></tr>
+</tbody></table>
+
+### Избегайте повторного преобразования строк в байты
+
+Не создавайте срезы байт из фиксированной строки повторно. Вместо этого
+выполните преобразование один раз и сохраните результат.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+for i := 0; i < b.N; i++ {
+ w.Write([]byte("Hello world"))
+}
+```
+
+</td><td>
+
+```go
+data := []byte("Hello world")
+for i := 0; i < b.N; i++ {
+ w.Write(data)
+}
+```
+
+</td></tr>
+<tr><td>
+
+```plain
+BenchmarkBad-4 50000000 22.2 ns/op
+```
+
+</td><td>
+
+```plain
+BenchmarkGood-4 500000000 3.25 ns/op
+```
+
+</td></tr>
+</tbody></table>
+
+### Предпочитайте указание ёмкости контейнеров
+
+По возможности указывайте ёмкость контейнеров, чтобы выделить память для
+контейнера заранее. Это минимизирует последующие выделения памяти (из-за
+копирования и изменения размера контейнера) при добавлении элементов.
+
+#### Указание подсказки ёмкости для карт
+
+По возможности предоставляйте подсказку ёмкости при инициализации карт с помощью
+`make()`.
+
+```go
+make(map[T1]T2, hint)
+```
+
+Предоставление подсказки ёмкости для `make()` пытается правильно определить
+размер карты при инициализации, что уменьшает необходимость её роста и выделений
+памяти при добавлении элементов.
+
+Обратите внимание, что в отличие от срезов, подсказки ёмкости для карт не
+гарантируют полного, упреждающего выделения, а используются для приблизительного
+определения количества необходимых корзин хэш-карты. Следовательно, выделения
+памяти всё ещё могут происходить при добавлении элементов в карту, даже до
+указанной ёмкости.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+m := make(map[string]os.FileInfo)
+
+files, _ := os.ReadDir("./files")
+for _, f := range files {
+ m[f.Name()] = f
+}
+```
+
+</td><td>
+
+```go
+
+files, _ := os.ReadDir("./files")
+
+m := make(map[string]os.DirEntry, len(files))
+for _, f := range files {
+ m[f.Name()] = f
+}
+```
+
+</td></tr>
+<tr><td>
+
+`m` создаётся без подсказки размера; при присваивании может быть больше
+выделений памяти.
+
+</td><td>
+
+`m` создаётся с подсказкой размера; при присваивании может быть меньше выделений
+памяти.
+
+</td></tr>
+</tbody></table>
+
+#### Указание ёмкости срезов
+
+По возможности предоставляйте подсказку ёмкости при инициализации срезов с
+помощью `make()`, особенно при использовании `append`.
+
+```go
+make([]T, length, capacity)
+```
+
+В отличие от карт, ёмкость среза — это не подсказка: компилятор выделит
+достаточно памяти для ёмкости среза, предоставленной в `make()`, что означает,
+что последующие операции `append()` не будут приводить к выделениям памяти (пока
+длина среза не совпадёт с ёмкостью, после чего любые добавления потребуют
+изменения размера для хранения дополнительных элементов).
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+for n := 0; n < b.N; n++ {
+ data := make([]int, 0)
+ for k := 0; k < size; k++{
+ data = append(data, k)
+ }
+}
+```
+
+</td><td>
+
+```go
+for n := 0; n < b.N; n++ {
+ data := make([]int, 0, size)
+ for k := 0; k < size; k++{
+ data = append(data, k)
+ }
+}
+```
+
+</td></tr>
+<tr><td>
+
+```plain
+BenchmarkBad-4 100000000 2.48s
+```
+
+</td><td>
+
+```plain
+BenchmarkGood-4 100000000 0.21s
+```
+
+</td></tr>
+</tbody></table>
+
+## Стиль
+
+### Избегайте слишком длинных строк
+
+Избегайте строк кода, которые заставляют читателей прокручивать по горизонтали
+или слишком сильно поворачивать голову.
+
+Мы рекомендуем мягкое ограничение длины строки в **99 символов**. Авторы должны
+стараться переносить строки до достижения этого предела, но это не строгое
+ограничение. Коду разрешено превышать этот лимит.
+
+### Будьте последовательны
+
+Некоторые рекомендации, изложенные в этом документе, можно оценить объективно;
+другие ситуативны, контекстны или субъективны.
+
+Прежде всего, **будьте последовательны**.
+
+Последовательный код легче поддерживать, легче осмыслить, требует меньше
+когнитивных усилий и легче переносить или обновлять по мере появления новых
+соглашений или исправления классов ошибок.
+
+И наоборот, наличие множества различных или конфликтующих стилей в одной кодовой
+базе создаёт накладные расходы на поддержку, неопределённость и когнитивный
+диссонанс, что напрямую может способствовать снижению скорости разработки,
+болезненным код-ревью и ошибкам.
+
+При применении этих рекомендаций к кодовой базе рекомендуется вносить изменения
+на уровне пакета (или выше): применение на уровне подпакета нарушает указанную
+выше проблему, внося несколько стилей в один код.
+
+### Группируйте схожие объявления
+
+Go поддерживает группировку схожих объявлений.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+import "a"
+import "b"
+```
+
+</td><td>
+
+```go
+import (
+ "a"
+ "b"
+)
+```
+
+</td></tr>
+</tbody></table>
+
+Это также относится к константам, переменным и объявлениям типов.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+
+const a = 1
+const b = 2
+
+
+
+var a = 1
+var b = 2
+
+
+
+type Area float64
+type Volume float64
+```
+
+</td><td>
+
+```go
+const (
+ a = 1
+ b = 2
+)
+
+var (
+ a = 1
+ b = 2
+)
+
+type (
+ Area float64
+ Volume float64
+)
+```
+
+</td></tr>
+</tbody></table>
+
+Группируйте только связанные объявления. Не группируйте несвязанные объявления.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+type Operation int
+
+const (
+ Add Operation = iota + 1
+ Subtract
+ Multiply
+ EnvVar = "MY_ENV"
+)
+```
+
+</td><td>
+
+```go
+type Operation int
+
+const (
+ Add Operation = iota + 1
+ Subtract
+ Multiply
+)
+
+const EnvVar = "MY_ENV"
+```
+
+</td></tr>
+</tbody></table>
+
+Группы не ограничены в том, где могут использоваться. Например, их можно
+использовать внутри функций.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+func f() string {
+ red := color.New(0xff0000)
+ green := color.New(0x00ff00)
+ blue := color.New(0x0000ff)
+
+ // ...
+}
+```
+
+</td><td>
+
+```go
+func f() string {
+ var (
+ red = color.New(0xff0000)
+ green = color.New(0x00ff00)
+ blue = color.New(0x0000ff)
+ )
+
+ // ...
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Исключение: Объявления переменных, особенно внутри функций, должны
+группироваться вместе, если они объявлены рядом с другими переменными. Делайте
+так для переменных, объявленных вместе, даже если они не связаны.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+func (c *client) request() {
+ caller := c.name
+ format := "json"
+ timeout := 5*time.Second
+ var err error
+
+ // ...
+}
+```
+
+</td><td>
+
+```go
+func (c *client) request() {
+ var (
+ caller = c.name
+ format = "json"
+ timeout = 5*time.Second
+ err error
+ )
+
+ // ...
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Порядок групп импорта
+
+Должно быть две группы импорта:
+
+- Стандартная библиотека
+- Все остальные
+
+Это группировка, применяемая по умолчанию в `goimports`.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+import (
+ "fmt"
+ "os"
+ "go.uber.org/atomic"
+ "golang.org/x/sync/errgroup"
+)
+```
+
+</td><td>
+
+```go
+import (
+ "fmt"
+ "os"
+
+ "go.uber.org/atomic"
+ "golang.org/x/sync/errgroup"
+)
+```
+
+</td></tr>
+</tbody></table>
+
+### Имена пакетов
+
+При именовании пакетов выбирайте имя, которое:
+
+- Состоит только из строчных букв. Без заглавных букв и подчёркиваний.
+- Не требует переименования с использованием именованных импортов в большинстве
+ мест вызова.
+- Короткое и ёмкое. Помните, что имя полностью указывается в каждом месте
+ вызова.
+- Не во множественном числе. Например, `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"
+)
+```
+
+Во всех остальных сценариях псевдонимы импорта следует избегать, если нет
+прямого конфликта между импортами.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+import (
+ "fmt"
+ "os"
+ runtimetrace "runtime/trace"
+
+ nettrace "golang.net/x/trace"
+)
+```
+
+</td><td>
+
+```go
+import (
+ "fmt"
+ "os"
+ "runtime/trace"
+
+ nettrace "golang.net/x/trace"
+)
+```
+
+</td></tr>
+</tbody></table>
+
+### Группировка и порядок функций
+
+- Функции должны быть отсортированы в приблизительном порядке вызовов.
+- Функции в файле должны быть сгруппированы по получателю.
+
+Следовательно, экспортируемые функции должны появляться первыми в файле после
+определений `struct`, `const`, `var`.
+
+`newXYZ()`/`NewXYZ()` может появиться после определения типа, но до остальных
+методов получателя.
+
+Поскольку функции группируются по получателю, простые вспомогательные функции
+должны появляться ближе к концу файла.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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{}
+}
+```
+
+</td><td>
+
+```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 {...}
+```
+
+</td></tr>
+</tbody></table>
+
+### Уменьшайте вложенность
+
+Код должен уменьшать вложенность, где это возможно, обрабатывая случаи
+ошибок/особые условия первыми и возвращаясь рано или продолжая цикл. Уменьшайте
+количество кода, вложенного на несколько уровней.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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)
+ }
+}
+```
+
+</td><td>
+
+```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()
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Избыточный Else
+
+Если переменная устанавливается в обеих ветках if, это можно заменить одним if.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+var a int
+if b {
+ a = 100
+} else {
+ a = 10
+}
+```
+
+</td><td>
+
+```go
+a := 10
+if b {
+ a = 100
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Объявления переменных верхнего уровня
+
+На верхнем уровне используйте стандартное ключевое слово `var`. Не указывайте
+тип, если он не отличается от типа выражения.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+var _s string = F()
+
+func F() string { return "A" }
+```
+
+</td><td>
+
+```go
+var _s = F()
+// Поскольку F уже указывает, что возвращает строку, нам не нужно снова указывать тип.
+
+func F() string { return "A" }
+```
+
+</td></tr>
+</tbody></table>
+
+Указывайте тип, если тип выражения не совпадает точно с желаемым типом.
+
+```go
+type myError struct{}
+
+func (myError) Error() string { return "error" }
+
+func F() myError { return myError{} }
+
+var _e error = F()
+// F возвращает объект типа myError, но мы хотим error.
+```
+
+### Префикс \_ для неэкспортируемых глобальных переменных
+
+Добавляйте префикс `_` к неэкспортируемым переменным и константам верхнего
+уровня, чтобы было ясно, что они являются глобальными символами, когда они
+используются.
+
+Обоснование: Переменные и константы верхнего уровня имеют область видимости
+пакета. Использование общего имени делает лёгким случайное использование
+неправильного значения в другом файле.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// foo.go
+
+const (
+ defaultPort = 8080
+ defaultUser = "user"
+)
+
+// bar.go
+
+func Bar() {
+ defaultPort := 9090
+ ...
+ fmt.Println("Default port", defaultPort)
+
+ // Мы не увидим ошибку компиляции, если первая строка Bar() будет удалена.
+}
+```
+
+</td><td>
+
+```go
+// foo.go
+
+const (
+ _defaultPort = 8080
+ _defaultUser = "user"
+)
+```
+
+</td></tr>
+</tbody></table>
+
+**Исключение**: Неэкспортируемые значения ошибок могут использовать префикс
+`err` без подчёркивания. См. [Именование ошибок](#именование-ошибок).
+
+### Встраивание в структурах
+
+Встроенные типы должны находиться в начале списка полей структуры, и должна быть
+пустая строка, отделяющая встроенные поля от обычных полей.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+type Client struct {
+ version int
+ http.Client
+}
+```
+
+</td><td>
+
+```go
+type Client struct {
+ http.Client
+
+ version int
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Встраивание должно обеспечивать ощутимую пользу, например, добавлять или
+расширять функциональность семантически-уместным способом. Оно должно делать это
+без каких-либо негативных последствий для пользователя (см. также: [Избегайте
+встраивания типов в публичные
+структуры](#избегайте-встраивания-типов-в-публичные-структуры)).
+
+Исключение: Мьютексы не должны встраиваться, даже в неэкспортируемые типы. См.
+также: [Нулевые значения мьютексов
+допустимы](#нулевые-значения-мьютексов-допустимы).
+
+Встраивание **НЕ должно**:
+
+- Быть чисто косметическим или ориентированным на удобство.
+- Усложнять создание или использование внешних типов.
+- Влиять на нулевые значения внешних типов. Если внешний тип имеет полезное
+ нулевое значение, он должен сохранять его после встраивания внутреннего типа.
+- Раскрывать несвязанные функции или поля внешнего типа как побочный эффект
+ встраивания внутреннего типа.
+- Раскрывать неэкспортируемые типы.
+- Влиять на семантику копирования внешних типов.
+- Менять API или семантику типов внешних типов.
+- Встраивать неканоническую форму внутреннего типа.
+- Раскрывать детали реализации внешнего типа.
+- Позволять пользователям наблюдать или контролировать внутренности типа.
+- Менять общее поведение внутренних функций через обёртывание таким образом,
+ который может удивить пользователей.
+
+Проще говоря, встраивайте осознанно и преднамеренно. Хороший тест: "все ли эти
+экспортируемые внутренние методы/поля были бы добавлены напрямую к внешнему
+типу"; если ответ "некоторые" или "нет", не встраивайте внутренний тип —
+используйте поле.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+type A struct {
+ // Плохо: A.Lock() и A.Unlock() теперь доступны, не предоставляют функциональной пользы и позволяют пользователям контролировать детали внутренностей A.
+ sync.Mutex
+}
+```
+
+</td><td>
+
+```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)
+}
+```
+
+</td></tr>
+<tr><td>
+
+```go
+type Book struct {
+ // Плохо: указатель меняет полезность нулевого значения
+ io.ReadWriter
+
+ // другие поля
+}
+
+// позже
+var b Book
+b.Read(...) // panic: nil pointer
+b.String() // panic: nil pointer
+b.Write(...) // panic: nil pointer
+```
+
+</td><td>
+
+```go
+type Book struct {
+ // Хорошо: имеет полезное нулевое значение
+ bytes.Buffer
+
+ // другие поля
+}
+
+// позже
+
+var b Book
+b.Read(...) // ok
+b.String() // ok
+b.Write(...) // ok
+```
+
+</td></tr>
+<tr><td>
+
+```go
+type Client struct {
+ sync.Mutex
+ sync.WaitGroup
+ bytes.Buffer
+ url.URL
+}
+```
+
+</td><td>
+
+```go
+type Client struct {
+ mtx sync.Mutex
+ wg sync.WaitGroup
+ buf bytes.Buffer
+ url url.URL
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Объявление локальных переменных
+
+Короткое объявление переменных (`:=`) должно использоваться, если переменная
+явно устанавливается в некоторое значение.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+var s = "foo"
+```
+
+</td><td>
+
+```go
+s := "foo"
+```
+
+</td></tr>
+</tbody></table>
+
+Однако бывают случаи, когда значение по умолчанию понятнее при использовании
+ключевого слова `var`. [Объявление пустых
+срезов](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices),
+например.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+func f(list []int) {
+ filtered := []int{}
+ for _, v := range list {
+ if v > 10 {
+ filtered = append(filtered, v)
+ }
+ }
+}
+```
+
+</td><td>
+
+```go
+func f(list []int) {
+ var filtered []int
+ for _, v := range list {
+ if v > 10 {
+ filtered = append(filtered, v)
+ }
+ }
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### nil — это валидный срез
+
+`nil` — это валидный срез длины 0. Это означает, что:
+
+- Не следует явно возвращать срез длины ноль. Возвращайте `nil` вместо этого.
+
+ <table>
+ <thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+ <tbody>
+ <tr><td>
+
+ ```go
+ if x == "" {
+ return []int{}
+ }
+ ```
+
+ </td><td>
+
+ ```go
+ if x == "" {
+ return nil
+ }
+ ```
+
+ </td></tr>
+ </tbody></table>
+
+- Чтобы проверить, пуст ли срез, всегда используйте `len(s) == 0`. Не проверяйте
+ на `nil`.
+
+ <table>
+ <thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+ <tbody>
+ <tr><td>
+
+ ```go
+ func isEmpty(s []string) bool {
+ return s == nil
+ }
+ ```
+
+ </td><td>
+
+ ```go
+ func isEmpty(s []string) bool {
+ return len(s) == 0
+ }
+ ```
+
+ </td></tr>
+ </tbody></table>
+
+- Нулевое значение (срез, объявленный с `var`) можно использовать сразу без
+`make()`.
+
+ <table>
+ <thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+ <tbody>
+ <tr><td>
+
+```go
+nums := []int{}
+// или, nums := make([]int)
+
+if add1 {
+ nums = append(nums, 1)
+}
+
+if add2 {
+ nums = append(nums, 2)
+}
+```
+
+</td><td>
+
+```go
+var nums []int
+
+if add1 {
+ nums = append(nums, 1)
+}
+
+if add2 {
+ nums = append(nums, 2)
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Помните, что хотя nil-срез является валидным срезом, он не эквивалентен
+выделенному срезу длины 0 — один является nil, а другой нет — и они могут
+обрабатываться по-разному в разных ситуациях (например, при сериализации).
+
+### Уменьшайте область видимости переменных
+
+По возможности уменьшайте область видимости переменных и констант. Не уменьшайте
+область видимости, если это противоречит [Уменьшению
+вложенности](#уменьшайте-вложенность).
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+err := os.WriteFile(name, data, 0644)
+if err != nil {
+ return err
+}
+```
+
+</td><td>
+
+```go
+if err := os.WriteFile(name, data, 0644); err != nil {
+ return err
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Если вам нужен результат вызова функции вне if, то не следует пытаться уменьшить
+область видимости.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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
+}
+```
+
+</td><td>
+
+```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
+```
+
+</td></tr>
+</tbody></table>
+
+Константам не нужно быть глобальными, если они не используются в нескольких
+функциях или файлах или являются частью внешнего контракта пакета.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+const (
+ _defaultPort = 8080
+ _defaultUser = "user"
+)
+
+func Bar() {
+ fmt.Println("Default port", _defaultPort)
+}
+```
+
+</td><td>
+
+```go
+func Bar() {
+ const (
+ defaultPort = 8080
+ defaultUser = "user"
+ )
+ fmt.Println("Default port", defaultPort)
+}
+```
+
+</td></tr>
+</tbody></table>
+
+### Избегайте «голых» параметров
+
+«Голые» параметры в вызовах функций могут ухудшать читаемость. Добавляйте
+комментарии в стиле C (`/* ... */`) для имён параметров, когда их значение
+неочевидно.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// func printInfo(name string, isLocal, done bool)
+
+printInfo("foo", true, true)
+```
+
+</td><td>
+
+```go
+// func printInfo(name string, isLocal, done bool)
+
+printInfo("foo", true /* isLocal */, true /* done */)
+```
+
+</td></tr>
+</tbody></table>
+
+Ещё лучше заменить «голые» типы `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), которые могут занимать
+несколько строк и включать кавычки. Используйте их, чтобы избежать ручного
+экранирования строк, которое гораздо труднее читать.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+wantError := "unknown name:\"test\""
+```
+
+</td><td>
+
+```go
+wantError := `unknown error:"test"`
+```
+
+</td></tr>
+</tbody></table>
+
+### Инициализация структур
+
+#### Используйте имена полей для инициализации структур
+
+Вы почти всегда должны указывать имена полей при инициализации структур. Теперь
+это обеспечивается [`go vet`](https://pkg.go.dev/cmd/vet).
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+k := User{"John", "Doe", true}
+```
+
+</td><td>
+
+```go
+k := User{
+ FirstName: "John",
+ LastName: "Doe",
+ Admin: true,
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Исключение: Имена полей _могут_ быть опущены в таблицах тестов, когда полей 3
+или меньше.
+
+```go
+tests := []struct{
+ op Operation
+ want string
+}{
+ {Add, "add"},
+ {Subtract, "subtract"},
+}
+```
+
+#### Опускайте поля с нулевыми значениями в структурах
+
+При инициализации структур с именами полей опускайте поля, имеющие нулевые
+значения, если они не предоставляют значимый контекст. В противном случае
+позвольте Go автоматически установить их в нулевые значения.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+user := User{
+ FirstName: "John",
+ LastName: "Doe",
+ MiddleName: "",
+ Admin: false,
+}
+```
+
+</td><td>
+
+```go
+user := User{
+ FirstName: "John",
+ LastName: "Doe",
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Это помогает уменьшить шум для читателей, опуская значения, которые являются
+стандартными в данном контексте. Указываются только значимые значения.
+
+Включайте нулевые значения, когда имена полей предоставляют значимый контекст.
+Например, тестовые случаи в [Табличных тестах](#табличные-тесты) могут выиграть
+от указания имён полей, даже когда они имеют нулевые значения.
+
+```go
+tests := []struct{
+ give string
+ want int
+}{
+ {give: "0", want: 0},
+ // ...
+}
+```
+
+#### Используйте `var` для структур с нулевыми значениями
+
+Когда все поля структуры опущены в объявлении, используйте форму `var` для
+объявления структуры.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+user := User{}
+```
+
+</td><td>
+
+```go
+var user User
+```
+
+</td></tr>
+</tbody></table>
+
+Это отличает структуры с нулевыми значениями от тех, у которых есть ненулевые
+поля, аналогично различию, создаваемому для [инициализации
+карт](#инициализация-карт), и соответствует тому, как мы предпочитаем [объявлять
+пустые срезы](https://go.dev/wiki/CodeReviewComments#declaring-empty-slices).
+
+#### Инициализация ссылок на структуры
+
+Используйте `&T{}` вместо `new(T)` при инициализации ссылок на структуры, чтобы
+это было согласовано с инициализацией структур.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+sval := T{Name: "foo"}
+
+// несогласованно
+sptr := new(T)
+sptr.Name = "bar"
+```
+
+</td><td>
+
+```go
+sval := T{Name: "foo"}
+
+sptr := &T{Name: "bar"}
+```
+
+</td></tr>
+</tbody></table>
+
+### Инициализация карт
+
+Предпочитайте `make(..)` для пустых карт и карт, заполняемых программно. Это
+делает инициализацию карт визуально отличной от объявления и позволяет легко
+добавить подсказку размера позже, если она доступна.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+var (
+ // m1 безопасна для чтения и записи;
+ // m2 вызовет панику при записи.
+ m1 = map[T1]T2{}
+ m2 map[T1]T2
+)
+```
+
+</td><td>
+
+```go
+var (
+ // m1 безопасна для чтения и записи;
+ // m2 вызовет панику при записи.
+ m1 = make(map[T1]T2)
+ m2 map[T1]T2
+)
+```
+
+</td></tr>
+<tr><td>
+
+Объявление и инициализация визуально похожи.
+
+</td><td>
+
+Объявление и инициализация визуально различны.
+
+</td></tr>
+</tbody></table>
+
+По возможности предоставляйте подсказку ёмкости при инициализации карт с помощью
+`make()`. См. [Указание подсказки ёмкости для
+карт](#указание-подсказки-ёмкости-для-карт) для получения дополнительной
+информации.
+
+С другой стороны, если карта содержит фиксированный список элементов,
+используйте литералы карт для её инициализации.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+m := make(map[T1]T2, 3)
+m[k1] = v1
+m[k2] = v2
+m[k3] = v3
+```
+
+</td><td>
+
+```go
+m := map[T1]T2{
+ k1: v1,
+ k2: v2,
+ k3: v3,
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Основное правило — использовать литералы карт при добавлении фиксированного
+набора элементов во время инициализации, в противном случае используйте `make`
+(и указывайте подсказку размера, если доступна).
+
+### Строки формата вне Printf
+
+Если вы объявляете строки формата для функций в стиле `Printf` вне строкового
+литерала, сделайте их значениями `const`.
+
+Это помогает `go vet` выполнять статический анализ строки формата.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+msg := "unexpected values %v, %v\n"
+fmt.Printf(msg, 1, 2)
+```
+
+</td><td>
+
+```go
+const msg = "unexpected values %v, %v\n"
+fmt.Printf(msg, 1, 2)
+```
+
+</td></tr>
+</tbody></table>
+
+### Именование функций в стиле 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) могут быть полезным
+паттерном для написания тестов, чтобы избежать дублирования кода, когда основная
+тестовая логика повторяется.
+
+Если тестируемую систему нужно проверить на соответствие _нескольким условиям_,
+где определённые части входных и выходных данных меняются, следует использовать
+табличные тесты, чтобы уменьшить избыточность и улучшить читаемость.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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)
+```
+
+</td><td>
+
+```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)
+ })
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Табличные тесты облегчают добавление контекста к сообщениям об ошибках,
+уменьшают дублирование логики и позволяют добавлять новые тестовые случаи.
+
+Мы следуем соглашению, что срез структур называется `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` для указания
+ожиданий ошибки.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```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)
+ })
+ }
+}
+```
+
+</td><td>
+
+```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")
+}
+```
+
+</td></tr>
+</tbody></table>
+
+Эта сложность делает тест более трудным для изменения, понимания и
+доказательства его корректности.
+
+Хотя строгих правил нет, читаемость и поддерживаемость всегда должны быть
+главными при выборе между табличными тестами и отдельными тестами для
+множественных входов/выходов системы.
+
+#### Параллельные тесты
+
+Параллельные тесты, как и некоторые специализированные циклы (например, те, что
+создают горутины или захватывают ссылки как часть тела цикла), должны заботиться
+о явном присваивании переменных цикла внутри области видимости цикла, чтобы
+гарантировать, что они содержат ожидаемые значения.
+
+```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` или значение,
+которое меняется во время их выполнения.
+
+<!-- TODO: Объяснить, как использовать _test пакеты. -->
+
+### Функциональные опции
+
+Функциональные опции — это паттерн, в котором вы объявляете непрозрачный тип
+`Option`, который записывает информацию в некоторую внутреннюю структуру. Вы
+принимаете переменное количество этих опций и действуете на основе полной
+информации, записанной опциями во внутренней структуре.
+
+Используйте этот паттерн для необязательных аргументов в конструкторах и других
+публичных API, которые, как вы предвидите, могут потребовать расширения,
+особенно если у вас уже есть три или более аргументов в этих функциях.
+
+<table>
+<thead><tr><th>Плохо</th><th>Хорошо</th></tr></thead>
+<tbody>
+<tr><td>
+
+```go
+// package db
+
+func Open(
+ addr string,
+ cache bool,
+ logger *zap.Logger
+) (*Connection, error) {
+ // ...
+}
+```
+
+</td><td>
+
+```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) {
+ // ...
+}
+```
+
+</td></tr>
+<tr><td>
+
+Параметры 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)
+```
+
+</td><td>
+
+Опции предоставляются только при необходимости.
+
+```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),
+)
+```
+
+</td></tr>
+</tbody></table>
+
+Наш рекомендуемый способ реализации этого паттерна — использование интерфейса
+`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)
+
+<!-- TODO: заменить это на структуры параметров и функциональные опции, когда использовать одно против другого -->
+
+## Линтинг
+
+Более важно, чем любой «благословленный» набор линтеров, — линтить
+последовательно по всей кодовой базе.
+
+Мы рекомендуем использовать следующие линтеры как минимум, потому что считаем,
+что они помогают выявить наиболее распространённые проблемы, а также
+устанавливают высокую планку качества кода, не будучи излишне предписывающими:
+
+- [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/), доступные для использования.
+Вышеуказанные линтеры рекомендуются в качестве базового набора, и мы поощряем
+команды добавлять любые дополнительные линтеры, которые имеют смысл для их
+проектов.