diff options
Diffstat (limited to '')
| -rw-r--r-- | content/pages/gostyleguide/google/decisions.md | 4057 |
1 files changed, 4057 insertions, 0 deletions
diff --git a/content/pages/gostyleguide/google/decisions.md b/content/pages/gostyleguide/google/decisions.md new file mode 100644 index 0000000..acb17b7 --- /dev/null +++ b/content/pages/gostyleguide/google/decisions.md @@ -0,0 +1,4057 @@ +--- +order: 2 +title: Google Go Style Guide — Решения +--- + +# Решения по стилю Go + +Оригинал: https://google.github.io/styleguide/go/decisions + +[Обзор](https://neonxp.ru/pages/gostyleguide/google/) | [Руководство](https://neonxp.ru/pages/gostyleguide/google/guide) | [Решения](https://neonxp.ru/pages/gostyleguide/google/decisions) | +[Лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices) + + +**Примечание:** Это часть серии документов, описывающих [Стиль Go](https://neonxp.ru/pages/gostyleguide/google/) в +Google. Этот документ является **[нормативным](https://neonxp.ru/pages/gostyleguide/google/#normative), но не +[каноническим](https://neonxp.ru/pages/gostyleguide/google/#canonical)** и подчиняется [основному руководству по +стилю](https://neonxp.ru/pages/gostyleguide/google/guide/). Подробнее см. [в обзоре](https://neonxp.ru/pages/gostyleguide/google/#about). + +<a id="about"></a> + +## Об этом документе + +В этом документе содержатся решения по стилю, призванные унифицировать и дать +стандартные рекомендации, пояснения и примеры для советов, которые дают +наставники по читаемости Go. + +Этот документ **не является исчерпывающим** и будет пополняться со временем. В +случаях, когда [основное руководство по стилю](https://neonxp.ru/pages/gostyleguide/google/guide/) противоречит приведенным +здесь рекомендациям, **руководство по стилю имеет приоритет**, и этот документ +должен быть обновлен соответственно. + +Полный набор документов по стилю Go см. в +[Обзоре](https://google.github.io/styleguide/go#about). + +Следующие разделы были перемещены из "Решений по стилю" в другие части +руководства: + +* **MixedCaps**: см. [guide#mixed-caps](https://neonxp.ru/pages/gostyleguide/google/guide/#mixed-caps) <a + id="mixed-caps"></a> + +* **Форматирование**: см. [guide#formatting](https://neonxp.ru/pages/gostyleguide/google/guide/#formatting) <a + id="formatting"></a> + +* **Длина строки**: см. [guide#line-length](https://neonxp.ru/pages/gostyleguide/google/guide/#line-length) <a + id="line-length"></a> + +<a id="naming"></a> + +## Именование + +Общие рекомендации по именованию см. в разделе об именовании в [основном +руководстве по стилю](https://neonxp.ru/pages/gostyleguide/google/guide/#naming). Следующие разделы дают дальнейшие +разъяснения по конкретным областям именования. + +<a id="underscores"></a> + +### Подчеркивания + +Имена в Go, как правило, не должны содержать подчеркиваний. Существует три +исключения из этого принципа: + +1. Имена пакетов, которые импортируются только сгенерированным кодом, могут + содержать подчеркивания. Подробнее о том, как выбирать имена многословных + пакетов, см. в разделе [имена пакетов](#package-names). +1. Имена тестовых (`Test`), бенчмарк (`Benchmark`) и примеров (`Example`) + функций в файлах `*_test.go` могут содержать подчеркивания. +1. Низкоуровневые библиотеки, взаимодействующие с операционной системой или + cgo, могут повторно использовать идентификаторы, как это сделано в + [`syscall`]. Ожидается, что это будет очень редко встречаться в большинстве + кодовых баз. + +**Примечание:** Имена файлов исходного кода не являются идентификаторами Go и не +должны следовать этим соглашениям. Они могут содержать подчеркивания. + +[`syscall`]: https://pkg.go.dev/syscall#pkg-constants + +<a id="package-names"></a> + +### Имена пакетов + +<a id="TOC-PackageNames"></a> + +В Go имена пакетов должны быть краткими и использовать только строчные буквы и +цифры (например, [`k8s`], [`oauth2`]). Многословные имена пакетов должны +оставаться целыми и в нижнем регистре (например, [`tabwriter`] вместо +`tabWriter`, `TabWriter` или `tab_writer`). + +Избегайте выбора имен пакетов, которые могут быть [затенены] часто используемыми +локальными именами переменных. Например, `usercount` — лучшее имя пакета, чем +`count`, так как `count` — часто используемое имя переменной. + +Имена пакетов Go не должны содержать подчеркиваний. Если вам нужно импортировать +пакет, который содержит их в своем имени (обычно из сгенерированного или +стороннего кода), его необходимо переименовать при импорте в имя, подходящее для +использования в коде Go. + +Исключением является то, что имена пакетов, которые импортируются только +сгенерированным кодом, могут содержать подчеркивания. Конкретные примеры +включают: + +* Использование суффикса `_test` для модульных тестов, проверяющих только + экспортированный API пакета (пакет `testing` называет это ["черным + ящиком"](https://pkg.go.dev/testing)). Например, пакет `linkedlist` должен + определять свои модульные тесты "черного ящика" в пакете с именем + `linkedlist_test` (не `linked_list_test`) + +* Использование подчеркиваний и суффикса `_test` для пакетов, содержащих + функциональные или интеграционные тесты. Например, интеграционный тест + сервиса связного списка может называться `linked_list_service_test` + +* Использование суффикса `_test` для [примеров документации на уровне + пакета](https://go.dev/blog/examples) + +[`tabwriter`]: https://pkg.go.dev/text/tabwriter +[`k8s`]: https://pkg.go.dev/k8s.io/client-go/kubernetes +[`oauth2`]: https://pkg.go.dev/golang.org/x/oauth2 +[shadowed]: best-practices#shadowing + +Избегайте неинформативных имен пакетов, таких как `util`, `utility`, `common`, +`helper`, `model`, `testhelper` и т.д., которые могут побуждать пользователей +пакета [переименовывать его при импорте](#import-renaming). См.: + +* [Рекомендации по так называемым "служебным + пакетам"](https://neonxp.ru/pages/gostyleguide/google/best-practices/#util-packages) +* [Go Tip #97: Что в + имени](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #108: Сила хорошего имени + пакета](https://google.github.io/styleguide/go/index.html#gotip) + +Когда импортированный пакет переименовывается (например, `import foopb +"path/to/foo_go_proto"`), локальное имя пакета должно соответствовать правилам +выше, так как локальное имя определяет, как на символы в пакете ссылаются в +файле. Если данный импорт переименован в нескольких файлах, особенно в одном и +том же или соседних пакетах, по возможности следует использовать одно и то же +локальное имя для согласованности. + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +См. также: [Пост в блоге Go об именах +пакетов](https://go.dev/blog/package-names). + +<a id="receiver-names"></a> + +### Имена получателей (Receiver) + +<a id="TOC-ReceiverNames"></a> + +Имена [получателей] (receiver) должны быть: + +* Краткими (обычно одна или две буквы) +* Сокращениями для самого типа +* Применяться последовательно для каждого получателя этого типа + +Длинное имя | Лучшее имя +----------------------------- | ------------------------- +`func (tray Tray)` | `func (t Tray)` +`func (info *ResearchInfo)` | `func (ri *ResearchInfo)` +`func (this *ReportWriter)` | `func (w *ReportWriter)` +`func (self *Scanner)` | `func (s *Scanner)` + +[получателей]: https://golang.org/ref/spec#Method_declarations + +<a id="constant-names"></a> + +### Имена констант + +Имена констант должны использовать [MixedCaps], как и все остальные имена в Go. +([Экспортируемые] константы начинаются с заглавной буквы, а неэкспортируемые — +со строчной.) Это применимо, даже если это нарушает соглашения в других языках. +Имена констант не должны быть производными от их значений и должны объяснять, +что означает это значение. + +```go +// Хорошо: +const MaxPacketSize = 512 + +const ( + ExecuteBit = 1 << iota + WriteBit + ReadBit +) +``` + +[MixedCaps]: guide#mixed-caps +[Экспортируемые]: https://tour.golang.org/basics/3 + +Не используйте имена констант не в стиле MixedCaps или константы с префиксом +`K`. + +```go +// Плохо: +const MAX_PACKET_SIZE = 512 +const kMaxBufferSize = 1024 +const KMaxUsersPergroup = 500 +``` + +Называйте константы в соответствии с их ролью, а не значениями. Если у константы +нет роли, кроме ее значения, то нет необходимости определять ее как константу. + +```go +// Плохо: +const Twelve = 12 + +const ( + UserNameColumn = "username" + GroupColumn = "group" +) +``` + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +<a id="initialisms"></a> + +### Аббревиатуры и акронимы (Initialisms) + +<a id="TOC-Initialisms"></a> + +Слова в именах, которые являются аббревиатурами или акронимами (например, `URL` +и `NATO`), должны иметь одинаковый регистр. `URL` должен появляться как `URL` +или `url` (как в `urlPony` или `URLPony`), но никогда как `Url`. Как общее +правило, идентификаторы (например, `ID` и `DB`) также должны быть написаны с +заглавной буквы, аналогично их использованию в английской прозе. + +* В именах с несколькими аббревиатурами (например, `XMLAPI`, потому что оно + содержит `XML` и `API`) каждая буква в данной аббревиатуре должна иметь один + регистр, но каждая аббревиатура в имени не обязана иметь одинаковый регистр. +* В именах с аббревиатурой, содержащей строчную букву (например, `DDoS`, + `iOS`, `gRPC`), аббревиатура должна отображаться, как в стандартной прозе, + если только вам не нужно изменить первую букву ради [экспортируемости + (exportedness)]. В этих случаях вся аббревиатура должна быть в одном + регистре (например, `ddos`, `IOS`, `GRPC`). + +[экспортируемости (exportedness)]: + https://golang.org/ref/spec#Exported_identifiers + +<!-- Keep this table narrow. If it must grow wider, replace with a list. --> + +Использование в английском | Область видимости | Правильно | Неправильно +-------------------------- | ----------------- | --------- | -------------------------------------- +XML API | Экспортировано | `XMLAPI` | `XmlApi`, `XMLApi`, `XmlAPI`, `XMLapi` +XML API | Не экспортировано | `xmlAPI` | `xmlapi`, `xmlApi` +iOS | Экспортировано | `IOS` | `Ios`, `IoS` +iOS | Не экспортировано | `iOS` | `ios` +gRPC | Экспортировано | `GRPC` | `Grpc` +gRPC | Не экспортировано | `gRPC` | `grpc` +DDoS | Экспортировано | `DDoS` | `DDOS`, `Ddos` +DDoS | Не экспортировано | `ddos` | `dDoS`, `dDOS` +ID | Экспортировано | `ID` | `Id` +ID | Не экспортировано | `id` | `iD` +DB | Экспортировано | `DB` | `Db` +DB | Не экспортировано | `db` | `dB` +Txn | Экспортировано | `Txn` | `TXN` + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +<a id="getters"></a> + +### Геттеры (Getters) + +<a id="TOC-Getters"></a> + +Имена функций и методов не должны использовать префикс `Get` или `get`, если +только базовое понятие не использует слово "get" (например, HTTP GET). +Предпочитайте начинать имя с существительного напрямую, например, используйте +`Counts` вместо `GetCounts`. + +Если функция включает выполнение сложных вычислений или удаленного вызова, +вместо `Get` можно использовать другое слово, например `Compute` или `Fetch`, +чтобы читателю было ясно, что вызов функции может занять время и может +блокироваться или завершиться неудачей. + +<!--#include file="/go/g3doc/style/includes/special-name-exception.md"--> + +<a id="variable-names"></a> + +### Имена переменных + +<a id="TOC-VariableNames"></a> + +Общее эмпирическое правило заключается в том, что длина имени должна быть +пропорциональна размеру его области видимости и обратно пропорциональна +количеству раз, которое оно используется в этой области. Переменная, созданная +на уровне файла, может потребовать несколько слов, тогда как переменная в +области видимости одного внутреннего блока может быть одним словом или даже +всего одним-двумя символами, чтобы сохранить код понятным и избежать лишней +информации. + +Вот приблизительный базовый уровень. Эти численные рекомендации не являются +строгими правилами. Применяйте суждение, основанное на контексте, [ясности] и +[лаконичности]. + +* Малая область видимости — это область, в которой выполняется одна или две + небольшие операции, скажем, 1-7 строк. +* Средняя область видимости — это несколько небольших или одна большая + операция, скажем, 8-15 строк. +* Большая область видимости — это одна или несколько больших операций, скажем, + 15-25 строк. +* Очень большая область видимости — это все, что занимает больше страницы + (скажем, более 25 строк). + +[ясности]: guide#clarity +[лаконичности]: guide#concision + +Имя, которое может быть совершенно понятным (например, `c` для счетчика) в +маленькой области видимости, может оказаться недостаточным в большей области и +потребует уточнения, чтобы напомнить читателю о его назначении дальше по коду. +Область видимости, в которой много переменных или переменных, представляющих +похожие значения или понятия, может потребовать более длинных имен переменных, +чем предполагает область видимости. + +Специфичность понятия также может помочь сохранить имя переменной кратким. +Например, если используется только одна база данных, короткое имя переменной +вроде `db`, которое обычно зарезервировано для очень малых областей видимости, +может оставаться совершенно понятным даже при очень большой области видимости. В +этом случае одно слово `database`, вероятно, приемлемо в зависимости от размера +области видимости, но не обязательно, поскольку `db` — очень распространенное +сокращение для этого слова с малым количеством альтернативных интерпретаций. + +Имя локальной переменной должно отражать то, что она содержит, и как она +используется в текущем контексте, а не откуда взялось значение. Например, часто +бывает, что лучшее локальное имя переменной не совпадает с именем поля структуры +или поля protobuf. + +В общем: + +* Однобуквенные имена, такие как `count` или `options`, — хорошая отправная + точка. +* Дополнительные слова могут быть добавлены для различения похожих имен, + например `userCount` и `projectCount`. +* Не просто выбрасывайте буквы, чтобы сэкономить на печати. Например, + `Sandbox` предпочтительнее, чем `Sbx`, особенно для экспортируемых имен. +* Опускайте [типы и слова, похожие на типы] из большинства имен переменных. + * Для числа `userCount` — лучшее имя, чем `numUsers` или `usersInt`. + * Для среза `users` — лучшее имя, чем `userSlice`. + * Допустимо включать квалификатор, похожий на тип, если в области + видимости есть две версии значения, например, у вас может быть ввод, + сохраненный в `ageString`, и `age` для распарсенного значения. +* Опускайте слова, которые ясны из [окружающего контекста]. Например, в + реализации метода `UserCount` локальная переменная с именем `userCount`, + вероятно, избыточна; `count`, `users` или даже `c` так же читаемы. + +[типы и слова, похожие на типы]: #repetitive-with-type +[окружающего контекста]: #repetitive-in-context + +<a id="v"></a> + +#### Однобуквенные имена переменных + +Однобуквенные имена переменных могут быть полезным инструментом для минимизации +[повторов](#repetition), но также могут сделать код излишне непрозрачным. +Ограничьте их использование случаями, когда полное слово очевидно и где было бы +излишне повторять его вместо однобуквенной переменной. + +В общем: + +* Для [переменной-получателя метода] предпочтительно одно- или двухбуквенное + имя. +* Использование знакомых имен переменных для распространенных типов часто + полезно: + * `r` для `io.Reader` или `*http.Request` + * `w` для `io.Writer` или `http.ResponseWriter` +* Однобуквенные идентификаторы допустимы в качестве целочисленных переменных + цикла, особенно для индексов (например, `i`) и координат (например, `x` и + `y`). +* Сокращения могут быть допустимыми идентификаторами цикла, если область + видимости короткая, например `for _, n := range nodes { ... }`. + +[переменной-получателя метода]: #receiver-names + +<a id="repetition"></a> + +### Повторы + +<!-- +Примечание для будущих редакторов: + +Не используйте термин "stutter" (заикание) для обозначения случаев, когда имя повторяется. +--> + +Исходный код Go должен избегать ненужных повторов. Один из распространенных +источников этого — повторяющиеся имена, которые часто включают ненужные слова +или повторяют свой контекст или тип. Сам код также может быть излишне +повторяющимся, если один и тот же или похожий сегмент кода появляется несколько +раз в непосредственной близости. + +Повторяющееся именование может принимать многие формы, включая: + +<a id="repetitive-with-package"></a> + +#### Имя пакета vs. экспортируемый символ + +При именовании экспортируемых символов имя пакета всегда видно за пределами +вашего пакета, поэтому избыточную информацию между ними следует сократить или +устранить. Если пакет экспортирует только один тип, и он назван в честь самого +пакета, каноническое имя для конструктора — `New`, если он требуется. + +> **Примеры:** Повторяющееся имя -> Лучшее имя +> +> * `widget.NewWidget` -> `widget.New` +> * `widget.NewWidgetWithName` -> `widget.NewWithName` +> * `db.LoadFromDatabase` -> `db.Load` +> * `goatteleportutil.CountGoatsTeleported` -> `gtutil.CountGoatsTeleported` +> или `goatteleport.Count` +> * `myteampb.MyTeamMethodRequest` -> `mtpb.MyTeamMethodRequest` или +> `myteampb.MethodRequest` + +<a id="repetitive-with-type"></a> + +#### Имя переменной vs. тип + +Компилятор всегда знает тип переменной, и в большинстве случаев читателю также +понятен тип переменной по тому, как она используется. Уточнять тип переменной +необходимо только если ее значение появляется дважды в одной и той же области +видимости. + +Повторяющееся имя | Лучшее имя +------------------------------- | ---------------------- +`var numUsers int` | `var users int` +`var nameString string` | `var name string` +`var primaryProject *Project` | `var primary *Project` + +Если значение появляется в нескольких формах, это можно уточнить либо с помощью +дополнительного слова, например `raw` и `parsed`, либо с помощью базового +представления: + +```go +// Хорошо: +limitRaw := r.FormValue("limit") +limit, err := strconv.Atoi(limitRaw) +``` + +```go +// Хорошо: +limitStr := r.FormValue("limit") +limit, err := strconv.Atoi(limitStr) +``` + +<a id="repetitive-in-context"></a> + +#### Внешний контекст vs. локальные имена + +Имена, включающие информацию из окружающего их контекста, часто создают лишний +шум без пользы. Имя пакета, имя метода, имя типа, имя функции, путь импорта и +даже имя файла могут предоставить контекст, который автоматически квалифицирует +все имена внутри. + +```go +// Плохо: +// В пакете "ads/targeting/revenue/reporting" +type AdsTargetingRevenueReport struct{} + +func (p *Project) ProjectName() string +``` + +```go +// Хорошо: +// В пакете "ads/targeting/revenue/reporting" +type Report struct{} + +func (p *Project) Name() string +``` + +```go +// Плохо: +// В пакете "sqldb" +type DBConnection struct{} +``` + +```go +// Хорошо: +// В пакете "sqldb" +type Connection struct{} +``` + +```go +// Плохо: +// В пакете "ads/targeting" +func Process(in *pb.FooProto) *Report { + adsTargetingID := in.GetAdsTargetingID() +} +``` + +```go +// Хорошо: +// В пакете "ads/targeting" +func Process(in *pb.FooProto) *Report { + id := in.GetAdsTargetingID() +} +``` + +Повторение, как правило, следует оценивать в контексте использования символа, а +не изолированно. Например, следующий код содержит множество имен, которые могут +быть хороши в некоторых обстоятельствах, но избыточны в контексте: + +```go +// Плохо: +func (db *DB) UserCount() (userCount int, err error) { + var userCountInt64 int64 + if dbLoadError := db.LoadFromDatabase("count(distinct users)", &userCountInt64); dbLoadError != nil { + return 0, fmt.Errorf("failed to load user count: %s", dbLoadError) + } + userCount = int(userCountInt64) + return userCount, nil +} +``` + +Вместо этого информацию об именах, которые ясны из контекста или использования, +часто можно опустить: + +```go +// Хорошо: +func (db *DB) UserCount() (int, error) { + var count int64 + if err := db.Load("count(distinct users)", &count); err != nil { + return 0, fmt.Errorf("failed to load user count: %s", err) + } + return int(count), nil +} +``` + +<a id="commentary"></a> + +## Комментарии + +Соглашения, касающиеся комментариев (что комментировать, какой стиль +использовать, как предоставлять исполняемые примеры и т.д.), предназначены для +поддержки удобства чтения документации публичного API. Подробнее см. [Effective +Go](http://golang.org/doc/effective_go.html#commentary). + +Раздел о [соглашениях по документации] в документе о лучших практиках +рассматривает это подробнее. + +**Лучшая практика:** Используйте [предпросмотр документации (doc preview)] во +время разработки и проверки кода, чтобы увидеть, является ли документация и +исполняемые примеры полезными и представлены ли они так, как вы ожидаете. + +**Совет:** Godoc использует очень мало специального форматирования; списки и +фрагменты кода обычно должны иметь отступ, чтобы избежать переноса строк. Кроме +отступа, декорации, как правило, следует избегать. + +[предпросмотр документации (doc preview)]: best-practices#documentation-preview +[соглашениям по документации]: best-practices#documentation-conventions + +<a id="comment-line-length"></a> + +### Длина строк комментария + +Убедитесь, что комментарии читаемы из исходного кода даже на узких экранах. + +Когда комментарий становится слишком длинным, рекомендуется разбить его на +несколько однострочных комментариев. По возможности стремитесь к комментариям, +которые будут хорошо читаться на терминале шириной 80 колонок, однако это не +жесткий предел; в Go нет фиксированного ограничения длины строки для +комментариев. Стандартная библиотека, например, часто предпочитает разрывать +комментарий по пунктуации, что иногда оставляет отдельные строки ближе к отметке +60-70 символов. + +Существует множество существующего кода, в котором комментарии превышают 80 +символов в длину. Эти рекомендации не следует использовать как оправдание для +изменения такого кода в ходе проверки на читаемость (см. +[согласованность](https://neonxp.ru/pages/gostyleguide/google/guide/#consistency)), хотя командам рекомендуется использовать +возможность обновлять комментарии в соответствии с этим руководством в рамках +других рефакторингов. Основная цель этого руководства — гарантировать, что все +наставники по читаемости Go дают одинаковые рекомендации, когда и если +рекомендации даются. + +Подробнее о комментариях см. в [этом посте из блога Go о документации]. + +[этом посте из блога Go о документации]: + https://blog.golang.org/godoc-documenting-go-code + +```text +# Хорошо: +// Это абзац комментария. +// Длина отдельных строк не имеет значения в Godoc; +// но выбор переноса делает его легко читаемым на узких экранах. +// +// Не беспокойтесь слишком о длинном URL: +// https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ +// +// Аналогично, если у вас есть другая информация, которая становится неудобной +// из-за слишком большого количества разрывов строк, используйте свое суждение и включите длинную строку, +// если она помогает, а не мешает. +``` + +Избегайте комментариев, которые будут многократно переноситься на маленьких +экранах, что ухудшает удобство чтения. + +```text +# Плохо: +// Это абзац комментария. Длина отдельных строк не имеет значения в Godoc; +// но выбор переноса создает неровные строки на узких экранах или при просмотре кода, +// что может раздражать, особенно в блоке комментариев, который будет переноситься +// многократно. +// +// Не беспокойтесь слишком о длинном URL: +// https://supercalifragilisticexpialidocious.example.com:8080/Animalia/Chordata/Mammalia/Rodentia/Geomyoidea/Geomyidae/ +``` + +<a id="doc-comments"></a> + +### Документирующие комментарии (Doc comments) + +<a id="TOC-DocComments"></a> + +Все экспортируемые имена верхнего уровня должны иметь документирующие +комментарии, как и неэкспортируемые объявления типов или функций с неочевидным +поведением или смыслом. Эти комментарии должны быть [полными предложениями], +которые начинаются с имени описываемого объекта. Артикль ("a", "an", "the") +может предшествовать имени, чтобы оно читалось более естественно. + +```go +// Хорошо: +// Request представляет запрос на выполнение команды. +type Request struct { ... + +// Encode записывает JSON-кодировку req в w. +func Encode(w io.Writer, req *Request) { ... +``` + +Документирующие комментарии появляются в [Godoc](https://pkg.go.dev/) и +выводятся в IDE, поэтому их следует писать для всех, кто использует пакет. + +[полными предложениями]: #comment-sentences + +Документирующий комментарий относится к следующему символу или группе полей, +если он появляется в структуре. + +```go +// Хорошо: +// Options настраивает сервис управления группами. +type Options struct { + // Общая настройка: + Name string + Group *FooGroup + + // Зависимости: + DB *sql.DB + + // Кастомизация: + LargeGroupThreshold int // опционально; по умолчанию: 10 + MinimumMembers int // опционально; по умолчанию: 2 +} +``` + +**Лучшая практика:** Если у вас есть документирующие комментарии для +неэкспортируемого кода, следуйте тому же обычаю, как если бы он был +экспортируемым (а именно, начиная комментарий с неэкспортируемого имени). Это +упрощает его экспорт позже простой заменой неэкспортируемого имени на новое +экспортируемое в комментариях и коде. + +<a id="comment-sentences"></a> + +### Предложения в комментариях + +<a id="TOC-CommentSentences"></a> + +Комментарии, которые являются полными предложениями, должны начинаться с +заглавной буквы и заканчиваться пунктуацией, как стандартные английские +предложения. (В качестве исключения можно начать предложение с +некапитализированного имени идентификатора, если оно иначе понятно. Такие +случаи, вероятно, лучше делать только в начале абзаца.) + +Комментарии, которые являются фрагментами предложений, не имеют таких требований +к пунктуации или капитализации. + +[Документирующие комментарии] всегда должны быть полными предложениями, и +поэтому всегда должны начинаться с заглавной буквы и заканчиваться пунктуацией. +Простые комментарии в конце строки (особенно для полей структуры) могут быть +простыми фразами, которые предполагают, что имя поля является подлежащим. + +```go +// Хорошо: +// Server обрабатывает подачу цитат из собрания сочинений Шекспира. +type Server struct { + // BaseDir указывает на базовый каталог, в котором хранятся работы Шекспира. + // + // Ожидается следующая структура каталога: + // {BaseDir}/manifest.json + // {BaseDir}/{name}/{name}-part{number}.txt + BaseDir string + + WelcomeMessage string // отображается при входе пользователя + ProtocolVersion string // проверяется для входящих запросов + PageLength int // строк на странице при печати (опционально; по умолчанию: 20) +} +``` + +[Документирующие комментарии]: #doc-comments + +<a id="examples"></a> + +### Примеры + +<a id="TOC-Examples"></a> + +Пакеты должны четко документировать предполагаемое использование. Постарайтесь +предоставить [исполняемый пример]; примеры появляются в Godoc. Исполняемые +примеры принадлежат тестовому файлу, а не файлу исходного кода продакшена. +Смотрите этот пример ([Godoc], [исходный код]). + +[исполняемый пример]: http://blog.golang.org/examples +[Godoc]: https://pkg.go.dev/time#example-Duration +[исходный код]: + https://cs.opensource.google/go/go/+/HEAD:src/time/example_test.go + +Если предоставить исполняемый пример нецелесообразно, пример кода может быть +предоставлен внутри комментариев к коду. Как и другие фрагменты кода и командной +строки в комментариях, он должен следовать стандартным соглашениям +форматирования. + +<a id="named-result-parameters"></a> + +### Именованные возвращаемые параметры + +<a id="TOC-NamedResultParameters"></a> + +При именовании параметров учитывайте, как сигнатуры функций отображаются в +Godoc. Имя самой функции и тип возвращаемых параметров часто достаточно ясны. + +```go +// Хорошо: +func (n *Node) Parent1() *Node +func (n *Node) Parent2() (*Node, error) +``` + +Если функция возвращает два или более параметра одного типа, добавление имен +может быть полезным. + +```go +// Хорошо: +func (n *Node) Children() (left, right *Node, err error) +``` + +Если вызывающая сторона должна выполнить действие над определенными +возвращаемыми параметрами, их именование может помочь подсказать, какое это +действие: + +```go +// Хорошо: +// WithTimeout возвращает контекст, который будет отменен не позднее, чем через длительность d +// от текущего момента. +// +// Вызывающая сторона должна обеспечить вызов возвращенной функции cancel, когда +// контекст больше не нужен, чтобы предотвратить утечку ресурсов. +func WithTimeout(parent Context, d time.Duration) (ctx Context, cancel func()) +``` + +В приведенном выше коде отмена — это конкретное действие, которое должна +выполнить вызывающая сторона. Однако, если бы возвращаемые параметры были +записаны просто как `(Context, func())`, было бы неясно, что подразумевается под +"функцией отмены". + +Не используйте именованные возвращаемые параметры, когда имена создают [ненужные +повторы](#repetitive-with-type). + +```go +// Плохо: +func (n *Node) Parent1() (node *Node) +func (n *Node) Parent2() (node *Node, err error) +``` + +Не называйте возвращаемые параметры, чтобы избежать объявления переменной внутри +функции. Эта практика приводит к излишней многословности API в обмен на +незначительную краткость реализации. + +[Голые возвраты (Naked returns)] допустимы только в маленькой функции. Как +только это функция среднего размера, будьте явны со своими возвращаемыми +значениями. Аналогично, не называйте возвращаемые параметры только потому, что +это позволяет вам использовать голые возвраты. [Ясность](https://neonxp.ru/pages/gostyleguide/google/guide/#clarity) всегда +важнее, чем сэкономить несколько строк в вашей функции. + +Всегда приемлемо назвать возвращаемый параметр, если его значение должно быть +изменено в отложенном замыкании. + +> **Совет:** Типы часто могут быть понятнее, чем имена в сигнатурах функций. +> [GoTip #38: Функции как именованные типы] демонстрирует это. +> +> В [`WithTimeout`] выше, реальный код использует [`CancelFunc`] вместо простого +> `func()` в списке возвращаемых параметров и требует небольших усилий для +> документирования. + +[Голые возвраты (Naked returns)]: https://tour.golang.org/basics/7 +[GoTip #38: Функции как именованные типы]: + https://google.github.io/styleguide/go/index.html#gotip +[`WithTimeout`]: https://pkg.go.dev/context#WithTimeout +[`CancelFunc`]: https://pkg.go.dev/context#CancelFunc + +<a id="package-comments"></a> + +### Комментарии к пакетам + +<a id="TOC-PackageComments"></a> + +Комментарии к пакету должны появляться непосредственно перед объявлением пакета, +без пустой строки между комментарием и именем пакета. Пример: + +```go +// Хорошо: +// Package math предоставляет базовые константы и математические функции. +// +// Этот пакет не гарантирует битовой идентичности результатов на разных архитектурах. +package math +``` + +Должен быть ровно один комментарий к пакету на пакет. Если пакет состоит из +нескольких файлов, ровно в одном из файлов должен быть комментарий к пакету. + +Комментарии для пакетов `main` имеют немного другую форму, где имя правила +`go_binary` в BUILD-файле заменяет имя пакета. + +```go +// Хорошо: +// Команда seed_generator — это утилита, которая генерирует файл сида Finch +// из набора JSON-конфигураций исследований. +package main +``` + +Другие стили комментариев допустимы, если имя бинарника точно такое же, как +написано в BUILD-файле. Когда имя бинарника — первое слово, его заглавная буква +обязательна, даже если оно не совпадает с написанием вызова в командной строке. + +```go +// Хорошо: +// Binary seed_generator ... +// Command seed_generator ... +// Program seed_generator ... +// The seed_generator command ... +// The seed_generator program ... +// Seed_generator ... +``` + +Советы: + +* Примеры вызовов командной строки и использования API могут быть полезной + документацией. Для форматирования Godoc сделайте отступ для строк + комментария, содержащих код. + +* Если нет очевидного основного файла или если комментарий к пакету необычайно + длинный, допустимо поместить документирующий комментарий в файл с именем + `doc.go`, содержащий только комментарий и объявление пакета. + +* Многострочные комментарии могут использоваться вместо нескольких + однострочных. Это в первую очередь полезно, если документация содержит + разделы, которые могут быть полезны для копирования и вставки из исходного + файла, как, например, примеры командной строки (для бинарников) и примеры + шаблонов. + + ```go + // Хорошо: + /* + Команда seed_generator — это утилита, которая генерирует файл сида Finch + из набора JSON-конфигураций исследований. + + seed_generator *.json | base64 > finch-seed.base64 + */ + package template + ``` + +* Комментарии, предназначенные для сопровождающих и относящиеся ко всему + файлу, обычно размещаются после объявлений импорта. Они не отображаются в + Godoc и не подчиняются приведенным выше правилам для комментариев к пакетам. + +<a id="imports"></a> + +## Импорты + +<a id="TOC-Imports"></a> + +<a id="import-renaming"></a> + +### Переименование импортов + +Импорты пакетов обычно не должны переименовываться, но есть случаи, когда они +должны быть переименованы или когда переименование улучшает читаемость. + +Локальные имена для импортированных пакетов должны следовать [рекомендациям по +именованию пакетов](#package-names), включая запрет на использование +подчеркиваний и заглавных букв. Старайтесь быть +[последовательными](https://neonxp.ru/pages/gostyleguide/google/guide/#consistency), всегда используя одно и то же локальное +имя для одного и того же импортированного пакета. + +Импортированный пакет *должен* быть переименован, чтобы избежать конфликта имен +с другими импортами. (Следствие из этого: [хорошие имена +пакетов](#package-names) не должны требовать переименования.) В случае конфликта +имен предпочтительнее переименовать наиболее локальный или специфичный для +проекта импорт. + +Сгенерированные пакеты протоколов буфера *должны* быть переименованы, чтобы +удалить подчеркивания из их имен, и их локальные имена должны иметь суффикс +`pb`. Подробнее см. [лучшие практики для proto и заглушек +(stubs)](https://neonxp.ru/pages/gostyleguide/google/best-practices/#import-protos). + +```go +// Хорошо: +import ( + foosvcpb "path/to/package/foo_service_go_proto" +) +``` + +Наконец, импортированный, несгенерированный пакет *может* быть переименован, +если он имеет неинформативное имя (например, `util` или `v1`) Делайте это +экономно: не переименовывайте пакет, если код, окружающий использование пакета, +передает достаточно контекста. По возможности предпочитайте рефакторинг самого +пакета с более подходящим именем. + +```go +// Хорошо: +import ( + core "github.com/kubernetes/api/core/v1" + meta "github.com/kubernetes/apimachinery/pkg/apis/meta/v1beta1" +) +``` + +Если вам нужно импортировать пакет, имя которого конфликтует с распространенным +локальным именем переменной, которое вы хотите использовать (например, `url`, +`ssh`), и вы хотите переименовать пакет, предпочтительный способ сделать это — +использовать суффикс `pkg` (например, `urlpkg`). Обратите внимание, что +возможно затенение пакета локальной переменной; это переименование необходимо +только если пакет все еще нужно использовать, когда такая переменная находится в +области видимости. + +<a id="import-grouping"></a> + +### Группировка импортов + +Импорты должны быть организованы в следующие группы по порядку: + +1. Пакеты стандартной библиотеки + +1. Другие пакеты (проекта и вендорные) + +1. Импорты Protocol Buffer (например, `fpb "path/to/foo_go_proto"`) + +1. Импорт для [побочных эффектов (side + effects)](https://go.dev/doc/effective_go#blank_import) (например, `_ + "path/to/package"`) + +```go +// Хорошо: +package main + +import ( + "fmt" + "hash/adler32" + "os" + + "github.com/dsnet/compress/flate" + "golang.org/x/text/encoding" + "google.golang.org/protobuf/proto" + + foopb "myproj/foo/proto/proto" + + _ "myproj/rpc/protocols/dial" + _ "myproj/security/auth/authhooks" +) +``` + +<a id="import-blank"></a> + +### "Пустой" импорт (`import _`) + +<a id="TOC-ImportBlank"></a> + +Пакеты, которые импортируются только для их побочных эффектов (используя +синтаксис `import _ "package"`), могут импортироваться только в главном пакете +(`main`) или в тестах, которые требуют их. + +Некоторые примеры таких пакетов: + +* [time/tzdata](https://pkg.go.dev/time/tzdata) + +* [image/jpeg](https://pkg.go.dev/image/jpeg) в коде обработки изображений + +Избегайте пустых импортов в библиотечных пакетах, даже если библиотека косвенно +зависит от них. Ограничение импортов с побочными эффектами главным пакетом +помогает контролировать зависимости и позволяет писать тесты, которые полагаются +на другой импорт без конфликта или лишних затрат на сборку. + +Следующие исключения являются единственными для этого правила: + +* Вы можете использовать пустой импорт, чтобы обойти проверку на запрещенные + импорты в [статическом анализаторе nogo]. + +* Вы можете использовать пустой импорт пакета + [embed](https://pkg.go.dev/embed) в исходном файле, который использует + директиву компилятора `//go:embed`. + +**Совет:** Если вы создаете библиотечный пакет, который косвенно зависит от +импорта с побочным эффектом в продакшене, задокументируйте предполагаемое +использование. + +[статическом анализаторе nogo]: + https://github.com/bazelbuild/rules_go/blob/master/go/nogo.rst + +<a id="import-dot"></a> + +### "Точечный" импорт (`import .`) + +<a id="TOC-ImportDot"></a> + +Форма `import .` — это языковая возможность, позволяющая переносить +идентификаторы, экспортированные из другого пакета, в текущий пакет без +квалификации. Подробнее см. [спецификацию +языка](https://go.dev/ref/spec#Import_declarations). + +Не **используйте** эту возможность в кодовой базе Google; это затрудняет +понимание, откуда взялась функциональность. + +```go +// Плохо: +package foo_test + +import ( + "bar/testutil" // также импортирует "foo" + . "foo" +) + +var myThing = Bar() // Bar определен в пакете foo; квалификация не нужна. +``` + +```go +// Хорошо: +package foo_test + +import ( + "bar/testutil" // также импортирует "foo" + "foo" +) + +var myThing = foo.Bar() +``` + +<a id="errors"></a> + +## Ошибки + +<a id="returning-errors"></a> + +### Возврат ошибок + +<a id="TOC-ReturningErrors"></a> + +Используйте `error` для сигнализации о том, что функция может завершиться +неудачей. По соглашению, `error` — последний параметр результата. + +```go +// Хорошо: +func Good() error { /* ... */ } +``` + +Возврат `nil` в качестве ошибки — идиоматический способ сигнализировать об +успешной операции, которая в противном случае могла бы завершиться неудачей. +Если функция возвращает ошибку, вызывающие стороны должны рассматривать все +не-ошибочные возвращаемые значения как неопределенные, если явно не +задокументировано иначе. Обычно не-ошибочные возвращаемые значения являются их +нулевыми значениями, но на это нельзя полагаться. + +```go +// Хорошо: +func GoodLookup() (*Result, error) { + // ... + if err != nil { + return nil, err + } + return res, nil +} +``` + +Экспортируемые функции, возвращающие ошибки, должны возвращать их, используя тип +`error`. Конкретные типы ошибок подвержены тонким ошибкам: конкретный `nil` +указатель может быть завернут в интерфейс и таким образом стать ненулевым +значением (см. [FAQ по Go на эту тему][nil error]). + +```go +// Плохо: +func Bad() *os.PathError { /*...*/ } +``` + +**Совет:** Функция, принимающая аргумент [`context.Context`], обычно должна +возвращать `error`, чтобы вызывающая сторона могла определить, была ли отменена +контекст во время выполнения функции. + +[nil error]: https://golang.org/doc/faq#nil_error + +<a id="error-strings"></a> + +### Строки ошибок + +<a id="TOC-ErrorStrings"></a> + +Строки ошибок не должны начинаться с заглавной буквы (если только не начинаются +с экспортируемого имени, имени собственного или аббревиатуры) и не должны +заканчиваться пунктуацией. Это потому что строки ошибок обычно появляются внутри +другого контекста перед выводом пользователю. + +```go +// Плохо: +err := fmt.Errorf("Something bad happened.") +``` + +```go +// Хорошо: +err := fmt.Errorf("something bad happened") +``` + +С другой стороны, стиль для полного отображаемого сообщения (логирование, +неудача теста, ответ API или другой UI) зависит от ситуации, но обычно должен +быть написан с заглавной буквы. + +```go +// Хорошо: +log.Infof("Operation aborted: %v", err) +log.Errorf("Operation aborted: %v", err) +t.Errorf("Op(%q) failed unexpectedly; err=%v", args, err) +``` + +<a id="handle-errors"></a> + +### Обработка ошибок + +<a id="TOC-HandleErrors"></a> + +Код, который сталкивается с ошибкой, должен принять осознанное решение о том, +как ее обработать. Обычно нецелесообразно отбрасывать ошибки, используя +переменные `_`. Если функция возвращает ошибку, сделайте одно из следующих +действий: + +* Обработайте и исправьте ошибку немедленно. +* Верните ошибку вызывающей стороне. +* В исключительных ситуациях вызовите [`log.Fatal`] или (если абсолютно + необходимо) `panic`. + +**Примечание:** `log.Fatalf` здесь не из стандартной библиотеки log. См. +[#logging]. + +В редких обстоятельствах, когда уместно игнорировать или отбросить ошибку +(например, вызов [`(*bytes.Buffer).Write`], который, как задокументировано, +никогда не завершается неудачей), сопровождающий комментарий должен объяснять, +почему это безопасно. + +```go +// Хорошо: +var b *bytes.Buffer + +n, _ := b.Write(p) // никогда не возвращает ненулевую ошибку +``` + +Для дальнейшего обсуждения и примеров обработки ошибок см. [Effective +Go](http://golang.org/doc/effective_go.html#errors) и [лучшие +практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/.md#error-handling). + +[`(*bytes.Buffer).Write`]: https://pkg.go.dev/bytes#Buffer.Write + +<a id="in-band-errors"></a> + +### Ошибки "в потоке" (In-band errors) + +<a id="TOC-In-Band-Errors"></a> + +В C и подобных языках распространено, что функции возвращают значения, такие как +-1, null или пустую строку, чтобы сигнализировать об ошибках или отсутствующих +результатах. Это известно как обработка ошибок "в потоке". + +```go +// Плохо: +// Lookup возвращает значение для ключа или -1, если нет соответствия для ключа. +func Lookup(key string) int +``` + +Неспособность проверить значение ошибки "в потоке" может привести к ошибкам и +может приписать ошибки не той функции. + +```go +// Плохо: +// Следующая строка возвращает ошибку, что Parse завершился неудачей для входного значения, +// тогда как на самом деле неудача в том, что нет соответствия для missingKey. +return Parse(Lookup(missingKey)) +``` + +Поддержка Go нескольких возвращаемых значений предоставляет лучшее решение (см. +[раздел Effective Go о множественных возвращаемых значениях]). Вместо того +чтобы требовать от клиентов проверять значение ошибки "в потоке", функция должна +возвращать дополнительное значение, чтобы указать, являются ли ее другие +возвращаемые значения валидными. Это возвращаемое значение может быть ошибкой +или булевым значением, когда объяснение не нужно, и должно быть последним +возвращаемым значением. + +```go +// Хорошо: +// Lookup возвращает значение для ключа или ok=false, если нет соответствия для ключа. +func Lookup(key string) (value string, ok bool) +``` + +Такой API предотвращает ошибочное написание вызывающей стороной +`Parse(Lookup(key))`, что вызывает ошибку времени компиляции, поскольку +`Lookup(key)` имеет 2 вывода. + +Возврат ошибок таким образом способствует более надежной и явной обработке +ошибок: + +```go +// Хорошо: +value, ok := Lookup(key) +if !ok { + return fmt.Errorf("no value for %q", key) +} +return Parse(value) +``` + +Некоторые функции стандартной библиотеки, например в пакете `strings`, +возвращают значения ошибок "в потоке". Это значительно упрощает код для +манипуляций со строками за счет необходимости большей внимательности от +программиста. В целом, код Go в кодовой базе Google должен возвращать +дополнительные значения для ошибок. + +[раздел Effective Go о множественных возвращаемых значениях]: + http://golang.org/doc/effective_go.html#multiple-returns + +<a id="indent-error-flow"></a> + +### Отступы для потока ошибок + +<a id="TOC-IndentErrorFlow"></a> + +Обрабатывайте ошибки перед продолжением выполнения остального кода. Это улучшает +читаемость кода, позволяя читателю быстро найти нормальный путь выполнения. Эта +же логика применяется к любому блоку, который проверяет условие, а затем +завершается терминальным условием (например, `return`, `panic`, `log.Fatal`). + +Код, который выполняется, если терминальное условие не выполнено, должен +появляться после блока `if` и не должен быть вложен в предложение `else`. + +```go +// Хорошо: +if err != nil { + // обработка ошибки + return // или continue, и т.д. +} +// нормальный код +``` + +```go +// Плохо: +if err != nil { + // обработка ошибки +} else { + // нормальный код, который выглядит ненормальным из-за отступа +} +``` + +> **Совет:** Если вы используете переменную более чем для нескольких строк кода, +> как правило, не стоит использовать стиль `if` с инициализатором. В этих +> случаях обычно лучше вынести объявление наружу и использовать стандартный `if` +> оператор: +> +> ```go +> // Хорошо: +> x, err := f() +> if err != nil { +> // обработка ошибки +> return +> } +> // много кода, который использует x +> // на нескольких строках +> ``` +> +> ```go +> // Плохо: +> if x, err := f(); err != nil { +> // обработка ошибки +> return +> } else { +> // много кода, который использует x +> // на нескольких строках +> } +> ``` + +Подробнее см. [Go Tip #1: Линия видимости (Line of Sight)] и [TotT: Reduce Code +Complexity by Reducing +Nesting](https://testing.googleblog.com/2017/06/code-health-reduce-nesting-reduce.html). + +[Go Tip #1: Линия видимости (Line of Sight)]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="language"></a> + +## Язык + +<a id="literal-formatting"></a> + +### Форматирование литералов + +Go обладает исключительно мощным [синтаксисом составных литералов], с помощью +которого можно выражать глубоко вложенные, сложные значения одним выражением. +По возможности следует использовать этот синтаксис литералов вместо построения +значений поле за полем. Форматирование `gofmt` для литералов, как правило, +довольно хорошее, но существуют некоторые дополнительные правила для сохранения +читаемости и поддерживаемости этих литералов. + +[синтаксисом составных литералов]: + https://golang.org/ref/spec#Composite_literals + +<a id="literal-field-names"></a> + +#### Имена полей + +Литералы структур должны указывать **имена полей** для типов, определенных вне +текущего пакета. + +* Включайте имена полей для типов из других пакетов. + + ```go + // Хорошо: + // https://pkg.go.dev/encoding/csv#Reader + r := csv.Reader{ + Comma: ',', + Comment: '#', + FieldsPerRecord: 4, + } + ``` + + Положение полей в структуре и полный набор полей (оба из которых необходимо + указать правильно, когда имена полей опущены) обычно не считаются частью + публичного API структуры; указание имени поля необходимо, чтобы избежать + ненужной связи. + + ```go + // Плохо: + r := csv.Reader{',', '#', 4, false, false, false, false} + ``` + +* Для локальных типов пакета имена полей не обязательны. + + ```go + // Хорошо: + okay := Type{42} + also := internalType{4, 2} + ``` + + Имена полей все же следует использовать, если это делает код яснее, и это + очень распространено. Например, структуру с большим количеством полей почти + всегда следует инициализировать с указанием имен полей. + + <!-- TODO: Maybe a better example here that doesn't have many fields. --> + + ```go + // Хорошо: + okay := StructWithLotsOfFields{ + field1: 1, + field2: "two", + field3: 3.14, + field4: true, + } + ``` + +<a id="literal-matching-braces"></a> + +#### Совпадение скобок + +Закрывающая часть пары фигурных скобок всегда должна появляться на строке с +таким же количеством отступов, как и открывающая скобка. Однострочные литералы +обязательно имеют это свойство. Когда литерал занимает несколько строк, +сохранение этого свойства сохраняет соответствие скобок для литералов таким же, +как соответствие скобок для обычных синтаксических конструкций Go, таких как +функции и операторы `if`. + +Самая распространенная ошибка в этой области — ставить закрывающую скобку на той +же строке, что и значение в многострочном литерале структуры. В этих случаях +строка должна заканчиваться запятой, а закрывающая скобка должна появляться на +следующей строке. + +```go +// Хорошо: +good := []*Type\{\{Key: "value"\}\} +``` + +```go +// Хорошо: +good := []*Type{ + {Key: "multi"}, + {Key: "line"}, +} +``` + +```go +// Плохо: +bad := []*Type{ + {Key: "multi"}, + {Key: "line"}} +``` + +```go +// Плохо: +bad := []*Type{ + { + Key: "value"}, +} +``` + +<a id="literal-cuddled-braces"></a> + +#### "Объединенные" скобки (Cuddled braces) + +Удаление пробела между фигурными скобками (так называемое "объединение" их) для +литералов срезов и массивов допустимо только когда оба следующих условия +истинны. + +* [Отступы совпадают](#literal-matching-braces) +* Внутренние значения также являются литералами или сборщиками protobuf (то + есть не переменной или другим выражением) + +```go +// Хорошо: +good := []*Type{ + { // Не объединены + Field: "value", + }, + { + Field: "value", + }, +} +``` + +```go +// Хорошо: +good := []*Type{\{ // Правильно объединены + Field: "value", +}, { + Field: "value", +}\} +``` + +```go +// Хорошо: +good := []*Type{ + first, // Не могут быть объединены + {Field: "second"}, +} +``` + +```go +// Хорошо: +okay := []*pb.Type{pb.Type_builder{ + Field: "first", // Сборщики Proto могут быть объединены для экономии вертикального пространства +}.Build(), pb.Type_builder{ + Field: "second", +}.Build()} +``` + +```go +// Плохо: +bad := []*Type{ + first, + { + Field: "second", + }\} +``` + +<a id="literal-repeated-type-names"></a> + +#### Повторяющиеся имена типов + +Повторяющиеся имена типов могут быть опущены в литералах срезов и карт (map). +Это может помочь уменьшить загромождение. Приемлемый случай для явного +повторения имен типов — это работа со сложным типом, который не является +распространенным в вашем проекте, когда повторяющиеся имена типов находятся на +строках, далеко отстоящих друг от друга, и могут напоминать читателю о +контексте. + +```go +// Хорошо: +good := []*Type{ + {A: 42}, + {A: 43}, +} +``` + +```go +// Плохо: +repetitive := []*Type{ + &Type{A: 42}, + &Type{A: 43}, +} +``` + +```go +// Хорошо: +good := map[Type1]*Type2{ + {A: 1}: {B: 2}, + {A: 3}: {B: 4}, +} +``` + +```go +// Плохо: +repetitive := map[Type1]*Type2{ + Type1{A: 1}: &Type2{B: 2}, + Type1{A: 3}: &Type2{B: 4}, +} +``` + +**Совет:** Если вы хотите удалить повторяющиеся имена типов в литералах +структур, вы можете запустить `gofmt -s`. + +<a id="literal-zero-value-fields"></a> + +#### Поля с нулевыми значениями + +[Нулевые] поля могут быть опущены в литералах структур, когда ясность не +теряется в результате. + +Хорошо спроектированные API часто используют конструкцию с нулевыми значениями +для улучшенной читаемости. Например, опускание трех полей с нулевыми значениями +из следующей структуры привлекает внимание к единственной опции, которая +указана. + +[Нулевые]: https://golang.org/ref/spec#The_zero_value + +```go +// Плохо: +import ( + "github.com/golang/leveldb" + "github.com/golang/leveldb/db" +) + +ldb := leveldb.Open("/my/table", &db.Options{ + BlockSize: 1<<16, + ErrorIfDBExists: true, + + // Все эти поля имеют свои нулевые значения. + BlockRestartInterval: 0, + Comparer: nil, + Compression: nil, + FileSystem: nil, + FilterPolicy: nil, + MaxOpenFiles: 0, + WriteBufferSize: 0, + VerifyChecksums: false, +}) +``` + +```go +// Хорошо: +import ( + "github.com/golang/leveldb" + "github.com/golang/leveldb/db" +) + +ldb := leveldb.Open("/my/table", &db.Options{ + BlockSize: 1<<16, + ErrorIfDBExists: true, +}) +``` + +Структуры в табличных тестах часто выигрывают от [явных имен полей], особенно +когда тестовая структура нетривиальна. Это позволяет автору опускать поля с +нулевыми значениями полностью, когда рассматриваемые поля не относятся к +тестовому случаю. Например, успешные тестовые случаи должны опускать любые поля, +связанные с ошибками или неудачами. В случаях, когда нулевое значение +необходимо для понимания тестового случая, таких как тестирование нулевых или +`nil` входных данных, имена полей должны быть указаны. + +[явных имен полей]: #literal-field-names + +**Лаконично** + +```go +tests := []struct { + input string + wantPieces []string + wantErr error +}{ + { + input: "1.2.3.4", + wantPieces: []string{"1", "2", "3", "4"}, + }, + { + input: "hostname", + wantErr: ErrBadHostname, + }, +} +``` + +**Явно** + +```go +tests := []struct { + input string + wantIPv4 bool + wantIPv6 bool + wantErr bool +}{ + { + input: "1.2.3.4", + wantIPv4: true, + wantIPv6: false, + }, + { + input: "1:2::3:4", + wantIPv4: false, + wantIPv6: true, + }, + { + input: "hostname", + wantIPv4: false, + wantIPv6: false, + wantErr: true, + }, +} +``` + +<a id="nil-slices"></a> + +### Nil-срезы + +Для большинства целей нет функциональной разницы между `nil` и пустым срезом. +Встроенные функции, такие как `len` и `cap`, ведут себя ожидаемо на `nil` +срезах. + +```go +// Хорошо: +import "fmt" + +var s []int // nil + +fmt.Println(s) // [] +fmt.Println(len(s)) // 0 +fmt.Println(cap(s)) // 0 +for range s {...} // нет операции + +s = append(s, 42) +fmt.Println(s) // [42] +``` + +Если вы объявляете пустой срез как локальную переменную (особенно если она может +быть источником возвращаемого значения), предпочитайте инициализацию nil, чтобы +снизить риск ошибок со стороны вызывающих. + +```go +// Хорошо: +var t []string +``` + +```go +// Плохо: +t := []string{} +``` + +Не создавайте API, которые вынуждают своих клиентов делать различие между nil и +пустым срезом. + +```go +// Хорошо: +// Ping проверяет доступность своих целей. +// Возвращает хосты, которые успешно ответили. +func Ping(hosts []string) ([]string, error) { ... } +``` + +```go +// Плохо: +// Ping проверяет доступность своих целей и возвращает список хостов, +// которые успешно ответили. Может быть пустым, если вход был пустым. +// nil означает, что произошла системная ошибка. +func Ping(hosts []string) []string { ... } +``` + +При проектировании интерфейсов избегайте различия между `nil` срезом и ненулевым +срезом нулевой длины, так как это может привести к тонким программным ошибкам. +Обычно это достигается с помощью `len` для проверки на пустоту, а не `== nil`. + +Эта реализация принимает как `nil`, так и срезы нулевой длины как "пустые": + +```go +// Хорошо: +// describeInts описывает s с заданным префиксом, если s не пуст. +func describeInts(prefix string, s []int) { + if len(s) == 0 { + return + } + fmt.Println(prefix, s) +} +``` + +Вместо того чтобы полагаться на различие как часть API: + +```go +// Плохо: +func maybeInts() []int { /* ... */ } + +// describeInts описывает s с заданным префиксом; передайте nil, чтобы полностью пропустить. +func describeInts(prefix string, s []int) { + // Поведение этой функции непреднамеренно меняется в зависимости от того, что + // возвращает maybeInts() в 'пустых' случаях (nil или []int{}). + if s == nil { + return + } + fmt.Println(prefix, s) +} + +describeInts("Here are some ints:", maybeInts()) +``` + +См. [ошибки "в потоке"] для дальнейшего обсуждения. + +[ошибки "в потоке"]: #in-band-errors + +<a id="indentation-confusion"></a> + +### Путаница с отступами + +Избегайте введения разрыва строки, если это приведет к выравниванию остальной +части строки с вложенным блоком кода. Если этого невозможно избежать, оставьте +пробел, чтобы отделить код в блоке от перенесенной строки. + +```go +// Плохо: +if longCondition1 && longCondition2 && + // Условия 3 и 4 имеют тот же отступ, что и код внутри if. + longCondition3 && longCondition4 { + log.Info("all conditions met") +} +``` + +См. следующие разделы для конкретных рекомендаций и примеров: + +* [Форматирование функций](#func-formatting) +* [Условные операторы и циклы](#conditional-formatting) +* [Форматирование литералов](#literal-formatting) + +<a id="func-formatting"></a> + +### Форматирование функций + +Сигнатура объявления функции или метода должна оставаться на одной строке, чтобы +избежать [путаницы с отступами](#indentation-confusion). + +Списки аргументов функций могут создавать одни из самых длинных строк в файле +исходного кода Go. Однако они предшествуют изменению отступа, и поэтому трудно +разорвать строку так, чтобы последующие строки не выглядели частью тела функции +запутанным образом: + +```go +// Плохо: +func (r *SomeType) SomeLongFunctionName(foo1, foo2, foo3 string, + foo4, foo5, foo6 int) { + foo7 := bar(foo1) + // ... +} +``` + +См. [лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#funcargs) для нескольких вариантов +сокращения мест вызовов функций, которые в противном случае имели бы много +аргументов. + +Строки часто можно сократить, вынося локальные переменные. + +```go +// Хорошо: +local := helper(some, parameters, here) +good := foo.Call(list, of, parameters, local) +``` + +Аналогично, вызовы функций и методов не должны разделяться исключительно на +основе длины строки. + +```go +// Хорошо: +good := foo.Call(long, list, of, parameters, all, on, one, line) +``` + +```go +// Плохо: +bad := foo.Call(long, list, of, parameters, + with, arbitrary, line, breaks) +``` + +По возможности избегайте добавления встроенных комментариев к конкретным +аргументам функций. Вместо этого используйте [структуру опций (option +struct)](https://neonxp.ru/pages/gostyleguide/google/best-practices/#option-structure) или добавьте больше деталей в +документацию функции. + +```go +// Хорошо: +good := server.New(ctx, server.Options{Port: 42}) +``` + +```go +// Плохо: +bad := server.New( + ctx, + 42, // Port +) +``` + +Если API нельзя изменить или если локальный вызов необычен (независимо от того, +слишком длинный вызов или нет), всегда допустимо добавлять разрывы строк, если +это помогает понять вызов. + +```go +// Хорошо: +canvas.RenderHeptagon(fillColor, + x0, y0, vertexColor0, + x1, y1, vertexColor1, + x2, y2, vertexColor2, + x3, y3, vertexColor3, + x4, y4, vertexColor4, + x5, y5, vertexColor5, + x6, y6, vertexColor6, +) +``` + +Обратите внимание, что строки в приведенном выше примере не переносятся на +определенную границу столбца, а группируются на основе координат вершин и цвета. + +Длинные строковые литералы внутри функций не должны разбиваться ради длины +строки. Для функций, включающих такие строки, разрыв строки можно добавить +после формата строки, а аргументы могут быть предоставлены на следующей или +последующих строках. Решение о том, где должны быть разрывы строк, лучше всего +принимать на основе семантических групп входных данных, а не исключительно на +основе длины строки. + +```go +// Хорошо: +log.Warningf("Database key (%q, %d, %q) incompatible in transaction started by (%q, %d, %q)", + currentCustomer, currentOffset, currentKey, + txCustomer, txOffset, txKey) +``` + +```go +// Плохо: +log.Warningf("Database key (%q, %d, %q) incompatible in"+ + " transaction started by (%q, %d, %q)", + currentCustomer, currentOffset, currentKey, txCustomer, + txOffset, txKey) +``` + +<a id="conditional-formatting"></a> + +### Условные операторы и циклы + +Оператор `if` не должен разбиваться на строки; многострочные условия `if` могут +привести к [путанице с отступами](#indentation-confusion). + +```go +// Плохо: +// Второй оператор if выровнен с кодом внутри блока if, что вызывает +// путаницу с отступами. +if db.CurrentStatusIs(db.InTransaction) && + db.ValuesEqual(db.TransactionKey(), row.Key()) { + return db.Errorf(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) +} +``` + +Если короткое замыкание не требуется, булевы операнды могут быть извлечены +напрямую: + +```go +// Хорошо: +inTransaction := db.CurrentStatusIs(db.InTransaction) +keysMatch := db.ValuesEqual(db.TransactionKey(), row.Key()) +if inTransaction && keysMatch { + return db.Error(db.TransactionError, "query failed: row (%v): key does not match transaction key", row) +} +``` + +Также могут быть другие локальные переменные, которые можно извлечь, особенно +если условие уже повторяется: + +```go +// Хорошо: +uid := user.GetUniqueUserID() +if db.UserIsAdmin(uid) || db.UserHasPermission(uid, perms.ViewServerConfig) || db.UserHasPermission(uid, perms.CreateGroup) { + // ... +} +``` + +```go +// Плохо: +if db.UserIsAdmin(user.GetUniqueUserID()) || db.UserHasPermission(user.GetUniqueUserID(), perms.ViewServerConfig) || db.UserHasPermission(user.GetUniqueUserID(), perms.CreateGroup) { + // ... +} +``` + +Операторы `if`, содержащие замыкания или многострочные литералы структур, должны +обеспечить, чтобы [скобки совпадали](#literal-matching-braces), чтобы избежать +[путаницы с отступами](#indentation-confusion). + +```go +// Хорошо: +if err := db.RunInTransaction(func(tx *db.TX) error { + return tx.Execute(userUpdate, x, y, z) +}); err != nil { + return fmt.Errorf("user update failed: %s", err) +} +``` + +```go +// Хорошо: +if _, err := client.Update(ctx, &upb.UserUpdateRequest{ + ID: userID, + User: user, +}); err != nil { + return fmt.Errorf("user update failed: %s", err) +} +``` + +Аналогично, не пытайтесь вставлять искусственные разрывы строк в операторы +`for`. Вы всегда можете позволить строке быть просто длинной, если нет +элегантного способа ее рефакторить: + +```go +// Хорошо: +for i, max := 0, collection.Size(); i < max && !collection.HasPendingWriters(); i++ { + // ... +} +``` + +Однако часто он есть: + +```go +// Хорошо: +for i, max := 0, collection.Size(); i < max; i++ { + if collection.HasPendingWriters() { + break + } + // ... +} +``` + +Операторы `switch` и `case` также должны оставаться на одной строке. + +```go +// Хорошо: +switch good := db.TransactionStatus(); good { +case db.TransactionStarting, db.TransactionActive, db.TransactionWaiting: + // ... +case db.TransactionCommitted, db.NoTransaction: + // ... +default: + // ... +} +``` + +```go +// Плохо: +switch bad := db.TransactionStatus(); bad { +case db.TransactionStarting, + db.TransactionActive, + db.TransactionWaiting: + // ... +case db.TransactionCommitted, + db.NoTransaction: + // ... +default: + // ... +} +``` + +Если строка чрезмерно длинная, сделайте отступ для всех вариантов (`case`) и +отделите их пустой строкой, чтобы избежать [путаницы с +отступами](#indentation-confusion): + +```go +// Хорошо: +switch db.TransactionStatus() { +case + db.TransactionStarting, + db.TransactionActive, + db.TransactionWaiting, + db.TransactionCommitted: + + // ... +case db.NoTransaction: + // ... +default: + // ... +} +``` + +В условных выражениях, сравнивающих переменную с константой, помещайте значение +переменной слева от оператора равенства: + +```go +// Хорошо: +if result == "foo" { + // ... +} +``` + +Вместо менее ясной формулировки, где константа идет первой (["условия в стиле +Йоды"](https://en.wikipedia.org/wiki/Yoda_conditions)): + +```go +// Плохо: +if "foo" == result { + // ... +} +``` + +<a id="copying"></a> + +### Копирование + +<a id="TOC-Copying"></a> + +Чтобы избежать неожиданного псевдонимного доступа (aliasing) и подобных ошибок, +будьте осторожны при копировании структуры из другого пакета. Например, объекты +синхронизации, такие как `sync.Mutex`, не должны копироваться. + +Тип `bytes.Buffer` содержит срез `[]byte` и, в качестве оптимизации для +небольших строк, небольшой массив байтов, на который может ссылаться срез. Если +вы скопируете `Buffer`, срез в копии может стать псевдонимом массива в +оригинале, вызывая неожиданные эффекты при последующих вызовах методов. + +В целом, не копируйте значение типа `T`, если его методы ассоциированы с +указательным типом, `*T`. + +```go +// Плохо: +b1 := bytes.Buffer{} +b2 := b1 +``` + +Вызов метода, принимающего получателя по значению, может скрыть копирование. +Когда вы создаете API, вам обычно следует принимать и возвращать типы-указатели, +если ваши структуры содержат поля, которые не должны копироваться. + +Эти примеры допустимы: + +```go +// Хорошо: +type Record struct { + buf bytes.Buffer + // другие поля опущены +} + +func New() *Record {...} + +func (r *Record) Process(...) {...} + +func Consumer(r *Record) {...} +``` + +Но эти обычно ошибочны: + +```go +// Плохо: +type Record struct { + buf bytes.Buffer + // другие поля опущены +} + + +func (r Record) Process(...) {...} // Создает копию r.buf + +func Consumer(r Record) {...} // Создает копию r.buf +``` + +Эти рекомендации также применимы к копированию `sync.Mutex`. + +<a id="dont-panic"></a> + +### Не используйте panic + +<a id="TOC-Don-t-Panic"></a> + +Не используйте `panic` для обычной обработки ошибок. Вместо этого используйте +`error` и множественные возвращаемые значения. См. [раздел Effective Go об +ошибках]. + +В `package main` и коде инициализации рассмотрите [`log.Exit`] для ошибок, +которые должны завершать программу (например, неверная конфигурация), поскольку +во многих из этих случаев трассировка стека не поможет читателю. Обратите +внимание, что [`log.Exit`] вызывает [`os.Exit`] и отложенные (deferred) функции +не будут выполнены. + +Для ошибок, указывающих на "невозможные" условия, а именно на ошибки, которые +всегда должны быть обнаружены при проверке кода и/или тестировании, функция +может разумно вернуть ошибку или вызвать [`log.Fatal`]. + +Также см. [когда panic допустим](https://neonxp.ru/pages/gostyleguide/google/best-practices/.md#when-to-panic). + +**Примечание:** `log.Fatalf` здесь не из стандартной библиотеки log. См. +[#logging]. + +[раздел Effective Go об ошибках]: http://golang.org/doc/effective_go.html#errors +[`os.Exit`]: https://pkg.go.dev/os#Exit + +<a id="must-functions"></a> + +### Функции Must + +Вспомогательные функции настройки, которые останавливают программу при неудаче, +следуют соглашению об именовании `MustXYZ` (или `mustXYZ`). Как правило, их +следует вызывать только на раннем этапе запуска программы, а не для таких вещей, +как пользовательский ввод, где предпочтительна обычная обработка ошибок Go. + +Это часто возникает для функций, вызываемых для инициализации переменных уровня +пакета исключительно во время [инициализации +пакета](https://golang.org/ref/spec#Package_initialization) (например, +[template.Must](https://golang.org/pkg/text/template/#Must) и +[regexp.MustCompile](https://golang.org/pkg/regexp/#MustCompile)). + +```go +// Хорошо: +func MustParse(version string) *Version { + v, err := Parse(version) + if err != nil { + panic(fmt.Sprintf("MustParse(%q) = _, %v", version, err)) + } + return v +} + +// "Константа" уровня пакета. Если бы мы хотели использовать `Parse`, нам пришлось бы +// устанавливать значение в `init`. +var DefaultVersion = MustParse("1.2.3") +``` + +То же соглашение может использоваться во вспомогательных функциях тестирования, +которые останавливают только текущий тест (используя `t.Fatal`). Такие помощники +часто удобны при создании тестовых значений, например, в полях структур +[табличных тестов](#table-driven-tests), поскольку функции, возвращающие ошибки, +не могут быть напрямую присвоены полю структуры. + +```go +// Хорошо: +func mustMarshalAny(t *testing.T, m proto.Message) *anypb.Any { + t.Helper() + any, err := anypb.New(m) + if err != nil { + t.Fatalf("mustMarshalAny(t, m) = %v; want %v", err, nil) + } + return any +} + +func TestCreateObject(t *testing.T) { + tests := []struct{ + desc string + data *anypb.Any + }{ + { + desc: "my test case", + // Создание значений непосредственно внутри тестовых случаев табличного теста. + data: mustMarshalAny(t, mypb.Object{}), + }, + // ... + } + // ... +} +``` + +В обоих этих случаях ценность этого шаблона в том, что помощники могут быть +вызваны в "знаковом" контексте. Этих помощников не следует вызывать в местах, +где трудно обеспечить перехват ошибки, или в контексте, где ошибка должна быть +[проверена](#handle-errors) (например, во многих обработчиках запросов). Для +постоянных входных данных это позволяет тестам легко убедиться, что аргументы +`Must` корректны, а для непостоянных входных данных это позволяет тестам +проверять, что ошибки [правильно обработаны или +распространены](https://neonxp.ru/pages/gostyleguide/google/best-practices/#error-handling). + +Где функции `Must` используются в тесте, их обычно следует [помечать как +тестовый помощник](#mark-test-helpers) и вызывать `t.Fatal` при ошибке (см. +[обработка ошибок во вспомогательных тестовых +функциях](https://neonxp.ru/pages/gostyleguide/google/best-practices/#test-helper-error-handling) для дополнительных +соображений по использованию этого). + +Их не следует использовать, когда [обычная обработка +ошибок](https://neonxp.ru/pages/gostyleguide/google/best-practices/#error-handling) возможна (включая некоторый рефакторинг): + +```go +// Плохо: +func Version(o *servicepb.Object) (*version.Version, error) { + // Вернуть ошибку вместо использования функций Must. + v := version.MustParse(o.GetVersionString()) + return dealiasVersion(v) +} +``` + +<a id="goroutine-lifetimes"></a> + +### Время жизни горутин + +<a id="TOC-GoroutineLifetimes"></a> + +Когда вы порождаете горутины, сделайте ясным, когда или завершаются ли они. + +Горутины могут "подтекать", блокируясь на отправке или получении по каналу. +Сборщик мусора не завершит горутину, заблокированную на канале, даже если +никакая другая горутина не имеет ссылки на этот канал. + +Даже когда горутины не подтекают, оставлять их активными, когда они больше не +нужны, может вызвать другие тонкие и трудные для диагностики проблемы. Отправка +в закрытый канал вызывает панику. + +```go +// Плохо: +ch := make(chan int) +ch <- 42 +close(ch) +ch <- 13 // panic +``` + +Изменение все еще используемых входных данных "после того, как результат не +нужен", может привести к гонкам данных. Оставление горутин активными на +произвольное время может привести к непредсказуемому использованию памяти. + +Параллельный код должен быть написан так, чтобы время жизни горутин было +очевидным. Обычно это означает сохранение кода, связанного с синхронизацией, в +пределах области видимости функции и вынесение логики в [синхронные функции]. +Если параллелизм все еще не очевиден, важно задокументировать, когда и почему +горутины завершаются. + +Код, следующий лучшим практикам использования контекста, часто помогает +прояснить это. Обычно это управляется с помощью [`context.Context`]: + +```go +// Хорошо: +func (w *Worker) Run(ctx context.Context) error { + var wg sync.WaitGroup + // ... + for item := range w.q { + // process возвращается самое позднее при отмене контекста. + wg.Add(1) + go func() { + defer wg.Done() + process(ctx, item) + }() + } + // ... + wg.Wait() // Предотвращает переживание порожденными горутинами этой функции. +} +``` + +Существуют другие варианты вышеизложенного, использующие простые сигнальные +каналы, такие как `chan struct{}`, синхронизированные переменные, [условные +переменные][rethinking-slides] и многое другое. Важная часть заключается в том, +что конец горутины очевиден для последующих сопровождающих. + +В отличие от этого, следующий код небрежен в отношении того, когда его +порожденные горутины завершаются: + +```go +// Плохо: +func (w *Worker) Run() { + // ... + for item := range w.q { + // process возвращается, когда завершается, если вообще завершится, возможно, не обрабатывая корректно + // переход состояния или завершение самой программы Go. + go process(item) + } + // ... +} +``` + +Этот код может выглядеть нормально, но у него есть несколько скрытых проблем: + +* Код, вероятно, имеет неопределенное поведение в продакшене, и программа + может не завершиться чисто, даже если операционная система освободит + ресурсы. + +* Код трудно осмысленно протестировать из-за неопределенного жизненного цикла. + +* Код может подтекать ресурсами, как описано выше. + +См. также: + +* [Никогда не запускайте горутину, не зная, как она остановится][cheney-stop] +* Переосмысление классических паттернов параллелизма: + [слайды][rethinking-slides], [видео][rethinking-video] +* [Когда программы на Go завершаются] +* [Соглашения по документации: Контексты] + +[синхронные функции]: #synchronous-functions +[cheney-stop]: + https://dave.cheney.net/2016/12/22/never-start-a-goroutine-without-knowing-how-it-will-stop +[rethinking-slides]: + https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view +[rethinking-video]: https://www.youtube.com/watch?v=5zXAHh5tJqQ +[Когда программы на Go завершаются]: https://changelog.com/gotime/165 +[Соглашения по документации: Контексты]: + best-practices.md#documentation-conventions-contexts + +<a id="interfaces"></a> + +### Интерфейсы + +<a id="TOC-Interfaces"></a> + +Интерфейсы Go обычно принадлежат пакету, который *потребляет* значения типа +интерфейса, а не пакету, который *реализует* тип интерфейса. Реализующий пакет +должен возвращать конкретные (обычно указатель или структура) типы. Таким +образом, новые методы могут быть добавлены к реализациям без необходимости +обширного рефакторинга. Подробнее см. [GoTip #49: Принимайте интерфейсы, +возвращайте конкретные типы]. + +Не экспортируйте [тестовый дубль (test double)][double types] реализации +интерфейса из API, который его потребляет. Вместо этого спроектируйте API так, +чтобы его можно было тестировать с помощью [публичного API] [реальной +реализации]. См. [GoTip #42: Создание заглушки для тестирования] для +подробностей. Даже когда нецелесообразно использовать реальную реализацию, может +не потребоваться вводить интерфейс, полностью покрывающий все методы в реальном +типе; потребитель может создать интерфейс, содержащий только нужные ему методы, +как показано в [GoTip #78: Минимально жизнеспособные интерфейсы]. + +Для тестирования пакетов, использующих Stubby RPC клиенты, используйте реальное +клиентское соединение. Если реальный сервер не может быть запущен в тесте, +внутренняя практика Google — получить реальное клиентское соединение с локальным +[тестовым дублем] с использованием внутреннего пакета rpctest (скоро будет!). + +Не определяйте интерфейсы до того, как они используются (см. [TotT: Code +Health: Eliminate YAGNI Smells][tott-438] ). Без реалистичного примера +использования слишком сложно увидеть, нужен ли интерфейс вообще, не говоря уже о +том, какие методы он должен содержать. + +Не используйте параметры типа интерфейса, если пользователям пакета не нужно +передавать различные типы для них. + +Не экспортируйте интерфейсы, которые не нужны пользователям пакета. + +**TODO:** Написать более подробный документ об интерфейсах и дать на него ссылку +здесь. + +[GoTip #42: Создание заглушки для тестирования]: + https://google.github.io/styleguide/go/index.html#gotip +[GoTip #49: Принимайте интерфейсы, возвращайте конкретные типы]: + https://google.github.io/styleguide/go/index.html#gotip +[GoTip #78: Минимально жизнеспособные интерфейсы]: + https://google.github.io/styleguide/go/index.html#gotip +[реальной реализации]: best-practices#use-real-transports +[публичному API]: + https://abseil.io/resources/swe-book/html/ch12.html#test_via_public_apis +[double types]: + https://abseil.io/resources/swe-book/html/ch13.html#techniques_for_using_test_doubles +[тестовым дублем]: + https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts +[tott-438]: + https://testing.googleblog.com/2017/08/code-health-eliminate-yagni-smells.html + +```go +// Хорошо: +package consumer // consumer.go + +type Thinger interface { Thing() bool } + +func Foo(t Thinger) string { ... } +``` + +```go +// Хорошо: +package consumer // consumer_test.go + +type fakeThinger struct{ ... } +func (t fakeThinger) Thing() bool { ... } +... +if Foo(fakeThinger{...}) == "x" { ... } +``` + +```go +// Плохо: +package producer + +type Thinger interface { Thing() bool } + +type defaultThinger struct{ ... } +func (t defaultThinger) Thing() bool { ... } + +func NewThinger() Thinger { return defaultThinger{ ... } } +``` + +```go +// Хорошо: +package producer + +type Thinger struct{ ... } +func (t Thinger) Thing() bool { ... } + +func NewThinger() Thinger { return Thinger{ ... } } +``` + +<a id="generics"></a> + +### Дженерики (Generics) + +Дженерики (формально называемые "[Параметрами типа]") разрешены там, где они +удовлетворяют вашим бизнес-требованиям. Во многих приложениях обычный подход с +использованием существующих языковых возможностей (срезы, карты, интерфейсы и +т.д.) работает так же хорошо без добавленной сложности, поэтому будьте осторожны +с преждевременным использованием. См. обсуждение [наименьшего механизма (least +mechanism)](https://neonxp.ru/pages/gostyleguide/google/guide/#least-mechanism). + +При введении экспортируемого API, использующего дженерики, убедитесь, что он +должным образом задокументирован. Настоятельно рекомендуется включать +мотивирующие исполняемые [примеры]. + +Не используйте дженерики только потому, что вы реализуете алгоритм или структуру +данных, которой не важен тип элементов. Если на практике инстанцируется только +один тип, начните с того, чтобы ваш код работал с этим типом без использования +дженериков вообще. Добавить полиморфизм позже будет проще по сравнению с +удалением абстракции, которая оказывается ненужной. + +Не используйте дженерики для создания предметно-ориентированных языков (DSL). В +частности, воздержитесь от введения фреймворков обработки ошибок, которые могут +значительно обременять читателей. Вместо этого предпочитайте установленные +методы [обработки ошибок](#errors). Для тестирования особенно остерегайтесь +введения [библиотек утверждений (assertion libraries)](#assert) или фреймворков, +которые приводят к менее полезным [сообщениям об ошибках +тестов](#useful-test-failures). + +В общем: + +* [Пишите код, не проектируйте типы]. Из выступления на GopherCon от Роберта + Грисемера и Яна Лэнса Тейлора. +* Если у вас есть несколько типов, которые разделяют полезный объединяющий + интерфейс, рассмотрите моделирование решения с использованием этого + интерфейса. Дженерики могут не понадобиться. +* В противном случае вместо того, чтобы полагаться на тип `any` и чрезмерное + [переключение типов (type switching)](https://tour.golang.org/methods/16), + рассмотрите дженерики. + +См. также: + +* [Использование дженериков в Go], выступление Яна Лэнса Тейлора + +* [Учебник по дженерикам] на сайте Go + +[Учебник по дженерикам]: https://go.dev/doc/tutorial/generics +[Параметрами типа]: https://go.dev/design/43651-type-parameters +[Использование дженериков в Go]: https://www.youtube.com/watch?v=nr8EpUO9jhw +[Пишите код, не проектируйте типы]: + https://www.youtube.com/watch?v=Pa_e9EeCdy8&t=1250s + +<a id="pass-values"></a> + +### Передача по значению + +<a id="TOC-PassValues"></a> + +Не передавайте указатели в качестве аргументов функций только для экономии +нескольких байтов. Если функция читает свой аргумент `x` только как `*x` на всем +протяжении, то аргумент не должен быть указателем. Распространенные примеры +этого включают передачу указателя на строку (`*string`) или указателя на +значение интерфейса (`*io.Reader`). В обоих случаях само значение имеет +фиксированный размер и может быть передано напрямую. + +Этот совет не применим к большим структурам или даже малым структурам, которые +могут увеличиться в размере. В частности, сообщения protocol buffer обычно +следует обрабатывать по указателю, а не по значению. Тип-указатель удовлетворяет +интерфейсу `proto.Message` (принимаемому `proto.Marshal`, `protocmp.Transform`, +и т.д.), а сообщения protocol buffer могут быть довольно большими и часто +увеличиваются со временем. + +<a id="receiver-type"></a> + +### Тип получателя (Receiver) + +<a id="TOC-ReceiverType"></a> + +[Получатель метода] может быть передан либо по значению, либо по указателю, как +если бы он был обычным параметром функции. Выбор между ними основан на том, в +составе какого [набора методов] метод должен находиться. + +[Получатель метода]: https://golang.org/ref/spec#Method_declarations +[набора методов]: https://golang.org/ref/spec#Method_sets + +**Корректность важнее скорости или простоты.** Бывают случаи, когда вы должны +использовать значение-указатель. В других случаях выбирайте указатели для +больших типов или в качестве будущего доказательства, если у вас нет хорошего +понимания того, как будет расти код, и используйте значения для простых [простых +старых данных]. + +Список ниже подробно описывает каждый случай: + +* Если получатель является срезом и метод не переслаивает (reslice) и не + перераспределяет (reallocate) срез, используйте значение, а не указатель. + + ```go + // Хорошо: + type Buffer []byte + + func (b Buffer) Len() int { return len(b) } + ``` + +* Если методу нужно мутировать получателя, получатель должен быть указателем. + + ```go + // Хорошо: + type Counter int + + func (c *Counter) Inc() { *c++ } + + // См. https://pkg.go.dev/container/heap. + type Queue []Item + + func (q *Queue) Push(x Item) { *q = append([]Item{x}, *q...) } + ``` + +* Если получатель является структурой, содержащей поля, которые [не могут быть + безопасно скопированы](#copying), используйте получатель-указатель. + Распространенные примеры — [`sync.Mutex`] и другие типы синхронизации. + + ```go + // Хорошо: + type Counter struct { + mu sync.Mutex + total int + } + + func (c *Counter) Inc() { + c.mu.Lock() + defer c.mu.Unlock() + c.total++ + } + ``` + + **Совет:** Проверьте [Godoc] типа для информации о том, безопасно ли или + небезопасно его копировать. + +* Если получатель является "большой" структурой или массивом, + получатель-указатель может быть более эффективным. Передача структуры + эквивалентна передаче всех ее полей или элементов в качестве аргументов + методу. Если это кажется слишком большим для [передачи по + значению](#pass-values), указатель — хороший выбор. + +* Для методов, которые будут вызываться или выполняться параллельно с другими + функциями, которые изменяют получателя, используйте значение, если эти + изменения не должны быть видны вашему методу; в противном случае используйте + указатель. + +* Если получатель является структурой или массивом, любой элемент которого + является указателем на что-то, что может быть изменено, предпочтите + получателя-указателя, чтобы сделать намерение изменяемости понятным для + читателя. + + ```go + // Хорошо: + type Counter struct { + m *Metric + } + + func (c *Counter) Inc() { + c.m.Add(1) + } + ``` + +* Если получатель является [встроенным типом], таким как целое число или + строка, который не нужно изменять, используйте значение. + + ```go + // Хорошо: + type User string + + func (u User) String() { return string(u) } + ``` + +* Если получатель является картой (map), функцией или каналом, используйте + значение, а не указатель. + + ```go + // Хорошо: + // См. https://pkg.go.dev/net/http#Header. + type Header map[string][]string + + func (h Header) Add(key, value string) { /* опущено */ } + ``` + +* Если получатель является "маленьким" массивом или структурой, которые + естественным образом являются типом значения без изменяемых полей и без + указателей, получатель по значению обычно является правильным выбором. + + ```go + // Хорошо: + // См. https://pkg.go.dev/time#Time. + type Time struct { /* опущено */ } + + func (t Time) Add(d Duration) Time { /* опущено */ } + ``` + +* В случае сомнений используйте получатель-указатель. + +В качестве общего руководства предпочитайте, чтобы методы для типа были либо все +указательными методами, либо всеми методами по значению. + +**Примечание:** Существует много дезинформации о том, может ли передача значения +или указателя в функцию повлиять на производительность. Компилятор может выбрать +передачу указателей на значения в стеке, а также копирование значений в стеке, +но эти соображения не должны перевешивать читаемость и корректность кода в +большинстве обстоятельств. Когда производительность действительно имеет +значение, важно профилировать оба подхода с реалистичным бенчмарком, прежде чем +решать, что один подход превосходит другой. + +[простых старых данных]: https://en.wikipedia.org/wiki/Passive_data_structure +[`sync.Mutex`]: https://pkg.go.dev/sync#Mutex +[встроенным типом]: https://pkg.go.dev/builtin + +<a id="switch-break"></a> + +### `switch` и `break` + +<a id="TOC-SwitchBreak"></a> + +Не используйте операторы `break` без целевых меток в конце предложений `switch`; +они избыточны. В отличие от C и Java, предложения `switch` в Go автоматически +завершаются, и для достижения поведения в стиле C требуется оператор +`fallthrough`. Используйте комментарий, а не `break`, если хотите прояснить цель +пустого предложения. + +```go +// Хорошо: +switch x { +case "A", "B": + buf.WriteString(x) +case "C": + // обрабатывается вне оператора switch +default: + return fmt.Errorf("unknown value: %q", x) +} +``` + +```go +// Плохо: +switch x { +case "A", "B": + buf.WriteString(x) + break // этот break избыточен +case "C": + break // этот break избыточен +default: + return fmt.Errorf("unknown value: %q", x) +} +``` + +> **Примечание:** Если предложение `switch` находится внутри цикла `for`, +> использование `break` внутри `switch` не выходит из охватывающего цикла `for`. +> +> ```go +> for { +> switch x { +> case "A": +> break // выходит из switch, не из цикла +> } +> } +> ``` +> +> Чтобы выйти из охватывающего цикла, используйте метку на операторе `for`: +> +> ```go +> loop: +> for { +> switch x { +> case "A": +> break loop // выходит из цикла +> } +> } +> ``` + +<a id="synchronous-functions"></a> + +### Синхронные функции + +<a id="TOC-SynchronousFunctions"></a> + +Синхронные функции возвращают свои результаты напрямую и завершают любые +обратные вызовы или операции с каналами перед возвратом. Предпочитайте +синхронные функции асинхронным. + +Синхронные функции сохраняют горутины локализованными внутри вызова. Это +помогает рассуждать об их времени жизни и избегать утечек и гонок данных. +Синхронные функции также легче тестировать, поскольку вызывающая сторона может +передать входные данные и проверить выходные без необходимости опроса или +синхронизации. + +При необходимости вызывающая сторона может добавить параллелизм, вызвав функцию +в отдельной горутине. Однако довольно сложно (иногда невозможно) убрать ненужный +параллелизм на стороне вызывающего. + +См. также: + +* "Переосмысление классических паттернов параллелизма", выступление Брайана + Миллса: [слайды][rethinking-slides], [видео][rethinking-video] + +<a id="type-aliases"></a> + +### Псевдонимы типов (Type aliases) + +<a id="TOC-TypeAliases"></a> + +Используйте *определение типа*, `type T1 T2`, чтобы определить новый тип. +Используйте [*псевдоним типа*], `type T1 = T2`, чтобы ссылаться на существующий +тип без определения нового типа. Псевдонимы типов редки; их основное +использование — помочь в переносе пакетов в новые места исходного кода. Не +используйте псевдонимы типов, когда это не нужно. + +[*псевдоним типа*]: http://golang.org/ref/spec#Type_declarations + +<a id="use-percent-q"></a> + +### Используйте %q + +<a id="TOC-UsePercentQ"></a> + +Функции форматирования Go (`fmt.Printf` и т.д.) имеют глагол `%q`, который +печатает строки внутри двойных кавычек. + +```go +// Хорошо: +fmt.Printf("value %q looks like English text", someText) +``` + +Предпочитайте использование `%q` вместо эквивалентного ручного способа с +использованием `%s`: + +```go +// Плохо: +fmt.Printf("value \"%s\" looks like English text", someText) +// Также избегайте вручную оборачивать строки одинарными кавычками: +fmt.Printf("value '%s' looks like English text", someText) +``` + +Использование `%q` рекомендуется в выводе, предназначенном для людей, где +входное значение может быть пустым или содержать управляющие символы. Тихую +пустую строку заметить очень сложно, но `""` явно выделяется как таковая. + +<a id="use-any"></a> + +### Используйте any + +Go 1.18 вводит тип `any` как [псевдоним] для `interface{}`. Поскольку это +псевдоним, `any` эквивалентен `interface{}` во многих ситуациях, а в других +легко взаимозаменяем через явное преобразование. Предпочитайте использовать +`any` в новом коде. + +[псевдоним]: + https://go.googlesource.com/proposal/+/master/design/18130-type-alias.md + +## Общие библиотеки + +<a id="flags"></a> + +### Флаги + +<a id="TOC-Flags"></a> + +Программы Go в кодовой базе Google используют внутренний вариант [стандартного +пакета `flag`]. Он имеет схожий интерфейс, но хорошо взаимодействует с +внутренними системами Google. Имена флагов в бинарных файлах Go должны +предпочитать использование подчеркиваний для разделения слов, хотя переменные, +хранящие значение флага, должны следовать стандартному стилю именования Go +([mixed caps]). Конкретно, имя флага должно быть в змеином регистре +(snake_case), а имя переменной должно быть эквивалентным именем в верблюжьем +регистре (camelCase). + +```go +// Хорошо: +var ( + pollInterval = flag.Duration("poll_interval", time.Minute, "Interval to use for polling.") +) +``` + +```go +// Плохо: +var ( + poll_interval = flag.Int("pollIntervalSeconds", 60, "Interval to use for polling in seconds.") +) +``` + +Флаги должны определяться только в `package main` или эквивалентном. + +Универсальные пакеты должны настраиваться с использованием API Go, а не путем +пробивания до интерфейса командной строки; не позволяйте импорту библиотеки +экспортировать новые флаги как побочный эффект. То есть предпочитайте явные +аргументы функций или присваивание полей структуры или, что гораздо реже и под +самым строгим контролем, экспортируемые глобальные переменные. В крайне редком +случае, когда необходимо нарушить это правило, имя флага должно четко указывать +пакет, который он настраивает. + +Если ваши флаги являются глобальными переменными, поместите их в свою +собственную группу `var`, следующую после раздела импортов. + +Существуют дополнительные обсуждения о лучших практиках создания [сложных CLI] с +подкомандами. + +См. также: + +* [Tip of the Week #45: Avoid Flags, Especially in Library Code][totw-45] +* [Go Tip #10: Configuration Structs and + Flags](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #80: Dependency Injection + Principles](https://google.github.io/styleguide/go/index.html#gotip) + +[стандартного пакета `flag`]: https://golang.org/pkg/flag/ +[mixed caps]: guide#mixed-caps +[сложных CLI]: best-practices#complex-clis +[totw-45]: https://abseil.io/tips/45 + +<a id="logging"></a> + +### Логирование + +Программы Go в кодовой базе Google используют вариант стандартного [`log`] +пакета. Он имеет схожий, но более мощный интерфейс и хорошо взаимодействует с +внутренними системами Google. Пакет с открытым исходным кодом этой библиотеки +доступен как [пакет `glog`], и проекты Google с открытым исходным кодом могут +использовать его, но в этом руководстве он повсюду упоминается как `log`. + +**Примечание:** Для аномальных завершений программы эта библиотека использует +`log.Fatal` для аварийного завершения с трассировкой стека и `log.Exit` для +остановки без нее. Здесь нет `log.Panic` функции, как в стандартной библиотеке. + +**Совет:** `log.Info(v)` эквивалентно `log.Infof("%v", v)`, и то же самое +относится к другим уровням логирования. Предпочитайте версию без форматирования, +когда вам нечего форматировать. + +См. также: + +* Лучшие практики по [логированию ошибок](https://neonxp.ru/pages/gostyleguide/google/best-practices/#error-logging) и + [пользовательским уровням детализации (verbosity + levels)](https://neonxp.ru/pages/gostyleguide/google/best-practices/#vlog) +* Когда и как использовать пакет log для [остановки + программы](https://neonxp.ru/pages/gostyleguide/google/best-practices/#checks-and-panics) + +[`log`]: https://pkg.go.dev/log +[`log/slog`]: https://pkg.go.dev/log/slog +[пакет `glog`]: https://pkg.go.dev/github.com/golang/glog +[`log.Exit`]: https://pkg.go.dev/github.com/golang/glog#Exit +[`log.Fatal`]: https://pkg.go.dev/github.com/golang/glog#Fatal + +<a id="contexts"></a> + +### Контексты (Contexts) + +<a id="TOC-Contexts"></a> + +Значения типа [`context.Context`] несут учетные данные безопасности, информацию +трассировки, сроки и сигналы отмены через границы API и процессов. В отличие от +C++ и Java, которые в кодовой базе Google используют локальное хранилище потоков +(thread-local storage), программы Go передают контексты явно вдоль всей цепочки +вызовов функций от входящих RPC и HTTP запросов к исходящим запросам. + +[`context.Context`]: https://pkg.go.dev/context + +При передаче в функцию или метод [`context.Context`] всегда является первым +параметром. + +```go +func F(ctx context.Context /* другие аргументы */) {} +``` + +Исключения: + +* В HTTP обработчике, где контекст берется из + [`req.Context()`](https://pkg.go.dev/net/http#Request.Context). +* В потоковых RPC методах, где контекст берется из потока. + + Код, использующий потоковый gRPC, получает контекст из метода `Context()` в + сгенерированном типе сервера, который реализует `grpc.ServerStream`. См. + [документацию по сгенерированному коду + gRPC](https://grpc.io/docs/languages/go/generated-code/). + +* В точках входа (entrypoint functions) (см. ниже примеры таких функций) + используйте [`context.Background()`] или, для тестов, + [`tb.Context()`](https://pkg.go.dev/testing#TB.Context). + + * В бинарных целях: `main` + * В общем коде и библиотеках: `init` + * В тестах: `TestXXX`, `BenchmarkXXX`, `FuzzXXX` + +> **Примечание**: Очень редко код в середине цепочки вызовов требует создания +> базового контекста самостоятельно с помощью [`context.Background()`]. Всегда +> предпочитайте брать контекст у вашего вызывающего, если это не неправильный +> контекст. +> +> Вы можете столкнуться с серверными библиотеками (реализация Stubby, gRPC или +> HTTP во фреймворке сервера Google для Go), которые создают новый объект +> контекста для каждого запроса. Эти контексты сразу заполняются информацией из +> входящего запроса, так что при передаче в обработчик запроса прикрепленные +> значения контекста были распространены к нему через сетевую границу от +> клиента-вызывающего. Более того, время жизни этих контекстов ограничено +> временем запроса: когда запрос завершен, контекст отменяется. +> +> Если вы не реализуете серверный фреймворк, вы не должны создавать контексты с +> помощью [`context.Background()`] в библиотечном коде. Вместо этого +> предпочитайте использование отсоединения контекста (context detachment), +> которое упоминается ниже, если доступен существующий контекст. Если вы +> думаете, что вам действительно нужно [`context.Background()`] вне функций +> точек входа, обсудите это со списком рассылки Google Go style, прежде чем +> приступать к реализации. + +Соглашение о том, что [`context.Context`] идет первым в функциях, также +применимо к тестовым помощникам. + +```go +// Хорошо: +func readTestFile(ctx context.Context, t *testing.T, path string) string {} +``` + +Не добавляйте поле context в тип структуры. Вместо этого добавьте параметр +context в каждый метод типа, которому нужно передавать его дальше. Единственное +исключение — для методов, чья сигнатура должна соответствовать интерфейсу в +стандартной библиотеке или в сторонней библиотеке вне контроля Google. Такие +случаи очень редки и должны быть обсуждены со списком рассылки Google Go style +до реализации и проверки на читаемость. + +**Примечание:** Go 1.24 добавил метод [`(testing.TB).Context()`]. В тестах +предпочитайте использовать [`(testing.TB).Context()`] вместо +[`context.Background()`] для предоставления начального [`context.Context`], +используемого тестом. Вспомогательные функции, настройка окружения или тестовых +дублей и другие функции, вызываемые из тела тестовой функции, которые требуют +контекст, должны иметь его явно переданным. + +[`(testing.TB).Context()`]: https://pkg.go.dev/testing#TB.Context + +Код в кодовой базе Google, который должен порождать фоновые операции, способные +выполняться после отмены родительского контекста, может использовать внутренний +пакет для отсоединения. Следите за [issue +#40221](https://github.com/golang/go/issues/40221) для обсуждений об +альтернативе с открытым исходным кодом. + +Поскольку контексты неизменяемы, нормально передавать один и тот же контекст в +несколько вызовов, которые разделяют один и тот же дедлайн, сигнал отмены, +учетные данные, родительскую трассировку и т.д. + +См. также: + +* [Контексты и структуры] + +[`context.Background()`]: https://pkg.go.dev/context/#Background +[Контексты и структуры]: https://go.dev/blog/context-and-structs + +<a id="custom-contexts"></a> + +#### Пользовательские контексты + +Не создавайте пользовательские типы контекстов и не используйте интерфейсы, +отличные от [`context.Context`], в сигнатурах функций. Для этого правила нет +исключений. + +Представьте, если бы каждая команда имела свой пользовательский контекст. Каждый +вызов функции из пакета `p` в пакет `q` должен был бы определять, как +преобразовать `p.Context` в `q.Context`, для всех пар пакетов `p` и `q`. Это +нецелесообразно и создает ошибки для людей, и это делает автоматизированные +рефакторинги, добавляющие параметры контекста, почти невозможными. + +Если у вас есть данные приложения для передачи, поместите их в параметр, в +получателе, в глобальных переменных или в значении `Context`, если они +действительно принадлежат там. Создание вашего собственного типа контекста +неприемлемо, поскольку оно подрывает способность команды Go заставить программы +Go правильно работать в продакшене. + +<a id="crypto-rand"></a> + +### crypto/rand + +<a id="TOC-CryptoRand"></a> + +Не используйте пакет `math/rand` для генерации ключей, даже одноразовых. Если не +задано начальное значение (seed), генератор полностью предсказуем. При задании +начального значения `time.Nanoseconds()` существует всего несколько бит +энтропии. Вместо этого используйте `Reader` из `crypto/rand`, и если вам нужен +текст, выводите в шестнадцатеричном или base64. + +```go +// Хорошо: +import ( + "crypto/rand" + // "encoding/base64" + // "encoding/hex" + "fmt" + + // ... +) + +func Key() string { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + log.Fatalf("Out of randomness, should never happen: %v", err) + } + return fmt.Sprintf("%x", buf) + // или hex.EncodeToString(buf) + // или base64.StdEncoding.EncodeToString(buf) +} +``` + +**Примечание:** `log.Fatalf` здесь не из стандартной библиотеки log. См. +[#logging]. + +<a id="useful-test-failures"></a> + +## Полезные сообщения об ошибках тестов + +<a id="TOC-UsefulTestFailures"></a> + +Должна быть возможность диагностировать неудачу теста без чтения исходного кода +теста. Тесты должны завершаться неудачей с полезными сообщениями, +детализирующими: + +* Что вызвало неудачу +* Какие входные данные привели к ошибке +* Фактический результат +* Что ожидалось + +Конкретные соглашения для достижения этой цели изложены ниже. + +<a id="assert"></a> + +### Библиотеки утверждений (Assertion libraries) + +<a id="TOC-Assert"></a> + +Не создавайте "библиотеки утверждений" в качестве помощников для тестирования. + +Библиотеки утверждений — это библиотеки, которые пытаются объединить проверку и +формирование сообщений об ошибках в тесте (хотя те же ловушки могут применяться +и к другим тестовым помощникам). Подробнее о различии между тестовыми +помощниками и библиотеками утверждений см. [лучшие +практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#test-functions). + +```go +// Плохо: +var obj BlogPost + +assert.IsNotNil(t, "obj", obj) +assert.StringEq(t, "obj.Type", obj.Type, "blogPost") +assert.IntEq(t, "obj.Comments", obj.Comments, 2) +assert.StringNotEq(t, "obj.Body", obj.Body, "") +``` + +Библиотеки утверждений имеют тенденцию либо останавливать тест рано (если +`assert` вызывает `t.Fatalf` или `panic`), либо опускать соответствующую +информацию о том, что тест получил правильно: + +```go +// Плохо: +package assert + +func IsNotNil(t *testing.T, name string, val any) { + if val == nil { + t.Fatalf("Data %s = nil, want not nil", name) + } +} + +func StringEq(t *testing.T, name, got, want string) { + if got != want { + t.Fatalf("Data %s = %q, want %q", name, got, want) + } +} +``` + +Сложные функции утверждений часто не предоставляют [полезные сообщения об +ошибках] и контекст, существующий внутри тестовой функции. Слишком много функций +утверждений и библиотек приводит к фрагментированному опыту разработчика: какую +библиотеку утверждений использовать, какой стиль формата вывода она должна +выдавать и т.д.? Фрагментация создает ненужную путаницу, особенно для +сопровождающих библиотек и авторов крупномасштабных изменений, которые отвечают +за исправление потенциальных поломок вниз по течению. Вместо создания +предметно-ориентированного языка для тестирования используйте сам Go. + +Библиотеки утверждений часто выносят сравнения и проверки равенства. +Предпочитайте использование стандартных библиотек, таких как [`cmp`] и [`fmt`], +вместо этого: + +```go +// Хорошо: +var got BlogPost + +want := BlogPost{ + Comments: 2, + Body: "Hello, world!", +} + +if !cmp.Equal(got, want) { + t.Errorf("Blog post = %v, want = %v", got, want) +} +``` + +Для более предметно-ориентированных помощников сравнения предпочитайте +возвращать значение или ошибку, которые можно использовать в сообщении об ошибке +теста, вместо передачи `*testing.T` и вызова его методов сообщения об ошибках: + +```go +// Хорошо: +func postLength(p BlogPost) int { return len(p.Body) } + +func TestBlogPost_VeritableRant(t *testing.T) { + post := BlogPost{Body: "I am Gunnery Sergeant Hartman, your senior drill instructor."} + + if got, want := postLength(post), 60; got != want { + t.Errorf("Length of post = %v, want %v", got, want) + } +} +``` + +**Лучшая практика:** Если бы `postLength` была нетривиальной, было бы разумно +тестировать ее напрямую, независимо от любых тестов, которые ее используют. + +См. также: + +* [Сравнение равенства и разницы (diffs)](#types-of-equality) +* [Печать разниц (diffs)](#print-diffs) +* Подробнее о различии между тестовыми помощниками и помощниками утверждений + см. [лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#test-functions) +* Раздел [Go FAQ] о [фреймворках тестирования] и их мнение об их отсутствии + +[полезные сообщения об ошибках]: #useful-test-failures +[`fmt`]: https://golang.org/pkg/fmt/ +[помеча как тестовый помощник]: #mark-test-helpers +[Go FAQ]: https://go.dev/doc/faq +[фреймворках тестирования]: https://go.dev/doc/faq#testing_framework + +<a id="identify-the-function"></a> + +### Идентификация функции + +В большинстве тестов сообщения об ошибках должны включать имя функции, которая +завершилась неудачей, даже если это кажется очевидным из имени тестовой функции. +Конкретно, ваше сообщение об ошибке должно быть `YourFunc(%v) = %v, want %v` +вместо просто `got %v, want %v`. + +<a id="identify-the-input"></a> + +### Идентификация входных данных + +В большинстве тестов сообщения об ошибках должны включать входные данные +функции, если они кратки. Если соответствующие свойства входных данных не +очевидны (например, потому что входные данные большие или непрозрачные), вы +должны называть свои тестовые случаи описанием того, что тестируется, и выводить +это описание как часть вашего сообщения об ошибке. + +<a id="got-before-want"></a> + +### Got перед want + +Вывод тестов должен включать фактическое значение, которое вернула функция, +перед печатью значения, которое ожидалось. Стандартный формат для вывода тестов +это `YourFunc(%v) = %v, want %v`. Там, где вы бы написали "actual" и "expected", +предпочитайте использовать слова "got" и "want" соответственно. + +Для разниц (diffs) направленность менее очевидна, и поэтому важно включить ключ, +чтобы помочь в интерпретации неудачи. См. [раздел о печати разниц]. Независимо +от порядка diff, который вы используете в своих сообщениях об ошибках, вы должны +явно указать его как часть сообщения об ошибке, поскольку существующий код +непоследователен в порядке. + +[раздел о печати разниц]: #print-diffs + +<a id="compare-full-structures"></a> + +### Полное сравнение структур + +Если ваша функция возвращает структуру (или любой тип данных с несколькими +полями, такой как срезы, массивы и карты), избегайте написания тестового кода, +который выполняет ручное поле-за-полем сравнение структуры. Вместо этого +сконструируйте данные, которые вы ожидаете, что ваша функция вернет, и +сравнивайте напрямую с помощью [глубокого сравнения (deep comparison)]. + +**Примечание:** Это не применяется, если ваши данные содержат нерелевантные +поля, которые затемняют намерение теста. + +Если вашу структуру нужно сравнивать на приблизительное (или эквивалентное +семантическое) равенство или она содержит поля, которые не могут быть сравнены +на равенство (например, если одно из полей — `io.Reader`), настройка сравнения +[`cmp.Diff`] или [`cmp.Equal`] с опциями [`cmpopts`], такими как +[`cmpopts.IgnoreInterfaces`], может удовлетворить ваши потребности +([пример](https://play.golang.org/p/vrCUNVfxsvF)). + +Если ваша функция возвращает несколько возвращаемых значений, вам не нужно +оборачивать их в структуру перед сравнением. Просто сравните возвращаемые +значения по отдельности и выведите их. + +```go +// Хорошо: +val, multi, tail, err := strconv.UnquoteChar(`\"Fran & Freddie's Diner\"`, '"') +if err != nil { + t.Fatalf(...) +} +if val != `"` { + t.Errorf(...) +} +if multi { + t.Errorf(...) +} +if tail != `Fran & Freddie's Diner"` { + t.Errorf(...) +} +``` + +[глубокого сравнения (deep comparison)]: #types-of-equality +[`cmpopts`]: https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts +[`cmpopts.IgnoreInterfaces`]: + https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#IgnoreInterfaces + +<a id="compare-stable-results"></a> + +### Сравнение стабильных результатов + +Избегайте сравнения результатов, которые могут зависеть от стабильности вывода +пакета, которым вы не владеете. Вместо этого тест должен сравнивать семантически +релевантную информацию, которая стабильна и устойчива к изменениям в +зависимостях. Для функциональности, возвращающей форматированную строку или +сериализованные байты, как правило, небезопасно предполагать, что вывод +стабилен. + +Например, [`json.Marshal`] может измениться (и менялся в прошлом) в конкретных +байтах, которые он выдает. Тесты, выполняющие строковое равенство на JSON +строке, могут сломаться, если пакет `json` изменит способ сериализации байтов. +Вместо этого более надежный тест будет анализировать содержимое JSON строки и +убеждаться, что оно семантически эквивалентно некоторой ожидаемой структуре +данных. + +[`json.Marshal`]: https://golang.org/pkg/encoding/json/#Marshal + +<a id="keep-going"></a> + +### Продолжать выполнение + +Тесты должны продолжать работать как можно дольше, даже после неудачи, чтобы +вывести все неудачные проверки за один запуск. Таким образом, разработчик, +который исправляет падающий тест, не должен перезапускать тест после исправления +каждой ошибки, чтобы найти следующую ошибку. + +Предпочитайте вызов `t.Error` вместо `t.Fatal` для сообщения о несоответствии. +При сравнении нескольких различных свойств вывода функции используйте `t.Error` +для каждого из этих сравнений. + +```go +// Хорошо: +gotMean, gotVariance, err := MyDistribution(input) +if err != nil { + t.Fatalf("MyDistribution(%v) returned unexpected error: %v", input, err) +} +if diff := cmp.Diff(wantMean, gotMean); diff != "" { + t.Errorf("MyDistribution(%v) returned unexpected difference in mean value (-want +got):\n%s", input, diff) +} +if diff := cmp.Diff(wantVariance, gotVariance); diff != "" { + t.Errorf("MyDistribution(%v) returned unexpected difference in variance value (-want +got):\n%s", input, diff) +} +``` + +Вызов `t.Fatal` в первую очередь полезен для сообщения о неожиданном условии +(таком как ошибка или несоответствие вывода), когда последующие неудачи были бы +бессмысленны или даже вводили бы исследователя в заблуждение. Обратите внимание, +как код ниже вызывает `t.Fatalf` и *затем* `t.Errorf`: + +```go +// Хорошо: +gotEncoded := Encode(input) +if gotEncoded != wantEncoded { + t.Fatalf("Encode(%q) = %q, want %q", input, gotEncoded, wantEncoded) + // Не имеет смысла декодировать из неожиданного закодированного ввода. +} +gotDecoded, err := Decode(gotEncoded) +if err != nil { + t.Fatalf("Decode(%q) returned unexpected error: %v", gotEncoded, err) +} +if gotDecoded != input { + t.Errorf("Decode(%q) = %q, want %q", gotEncoded, gotDecoded, input) +} +``` + +Для табличных тестов рассмотрите использование подтестов и используйте `t.Fatal` +вместо `t.Error` и `continue`. См. также [GoTip #25: Subtests: Making Your Tests +Lean](https://google.github.io/styleguide/go/index.html#gotip). + +**Лучшая практика:** Для дальнейшего обсуждения о том, когда следует +использовать `t.Fatal`, см. [лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#t-fatal). + +<a id="types-of-equality"></a> + +### Сравнение равенства и разницы (diffs) + +Оператор `==` вычисляет равенство, используя [определенные языком сравнения]. +Скалярные значения (числа, булевы и т.д.) сравниваются на основе их значений, но +только некоторые структуры и интерфейсы можно сравнивать таким образом. +Указатели сравниваются на основе того, указывают ли они на одну и ту же +переменную, а не на основе равенства значений, на которые они указывают. + +Пакет [`cmp`] может сравнивать более сложные структуры данных, не обрабатываемые +адекватно `==`, такие как срезы. Используйте [`cmp.Equal`] для сравнения +равенства и [`cmp.Diff`] для получения читаемой человеком разницы между +объектами. + +```go +// Хорошо: +want := &Doc{ + Type: "blogPost", + Comments: 2, + Body: "This is the post body.", + Authors: []string{"isaac", "albert", "emmy"}, +} +if !cmp.Equal(got, want) { + t.Errorf("AddPost() = %+v, want %+v", got, want) +} +``` + +Как универсальная библиотека сравнения, `cmp` может не знать, как сравнивать +определенные типы. Например, она может сравнивать сообщения protobuf только если +передана опция [`protocmp.Transform`]. + +<!-- The order of want and got here is deliberate. See comment in #print-diffs. --> + +```go +// Хорошо: +if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("Foo() returned unexpected difference in protobuf messages (-want +got):\n%s", diff) +} +``` + +Хотя пакет `cmp` не является частью стандартной библиотеки Go, он поддерживается +командой Go и должен обеспечивать стабильные результаты равенства с течением +времени. Он настраивается пользователем и должен удовлетворять большинству +потребностей в сравнении. + +[определенные языком сравнения]: http://golang.org/ref/spec#Comparison_operators +[`cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[`cmp.Equal`]: https://pkg.go.dev/github.com/google/go-cmp/cmp#Equal +[`cmp.Diff`]: https://pkg.go.dev/github.com/google/go-cmp/cmp#Diff +[`protocmp.Transform`]: + https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp#Transform + +Существующий код может использовать следующие более старые библиотеки и может +продолжать использовать их для согласованности: + +* [`pretty`] создает эстетически приятные отчеты о различиях. Однако он + довольно намеренно считает значения, имеющие одинаковое визуальное + представление, равными. В частности, `pretty` не улавливает различия между + nil срезами и пустыми, не чувствителен к разным реализациям интерфейсов с + идентичными полями, и возможно использовать вложенную карту в качестве + основы для сравнения со значением структуры. Он также сериализует все + значение в строку перед созданием diff, и, как таковой, не является хорошим + выбором для сравнения больших значений. По умолчанию он сравнивает + неэкспортируемые поля, что делает его чувствительным к изменениям в деталях + реализации в ваших зависимостях. По этой причине нецелесообразно + использовать `pretty` на сообщениях protobuf. + +[`pretty`]: https://pkg.go.dev/github.com/kylelemons/godebug/pretty + +Предпочитайте использовать `cmp` для нового кода, и стоит рассмотреть обновление +старого кода для использования `cmp`, где и когда это практично. + +Старый код может использовать функцию стандартной библиотеки `reflect.DeepEqual` +для сравнения сложных структур. `reflect.DeepEqual` не следует использовать для +проверки равенства, так как она чувствительна к изменениям в неэкспортированных +полях и другим деталям реализации. Код, использующий `reflect.DeepEqual`, должен +быть обновлен до одной из вышеупомянутых библиотек. + +**Примечание:** Пакет `cmp` предназначен для тестирования, а не для +использования в продакшене. Как таковой, он может вызывать панику, когда +подозревает, что сравнение выполнено неправильно, чтобы дать инструкции +пользователям, как улучшить тест, чтобы он был менее хрупким. Учитывая +склонность cmp к панике, он непригоден для кода, который используется в +продакшене, поскольку ложная паника может быть фатальной. + +<a id="level-of-detail"></a> + +### Уровень детализации + +Обычное сообщение об ошибке, подходящее для большинства тестов Go, это +`YourFunc(%v) = %v, want %v`. Однако бывают случаи, которые могут потребовать +больше или меньше деталей: + +* Тесты, выполняющие сложные взаимодействия, должны также описывать + взаимодействия. Например, если один и тот же `YourFunc` вызывается + несколько раз, идентифицируйте, какой вызов провалил тест. Если важно знать + любое дополнительное состояние системы, включите это в вывод ошибки (или + хотя бы в логи). +* Если данные представляют собой сложную структуру со значительным шаблонным + кодом, допустимо описать только важные части в сообщении, но не чрезмерно + затемняйте данные. +* Ошибки настройки не требуют такого же уровня детализации. Если тестовый + помощник заполняет таблицу Spanner, но Spanner был выключен, вам, вероятно, + не нужно включать, какие тестовые входные данные вы собирались сохранить в + базе данных. `t.Fatalf("Setup: Failed to set up test database: %s", err)` + обычно достаточно полезно, чтобы решить проблему. + +**Совет:** Заставьте ваш режим отказа срабатывать во время разработки. +Просмотрите, как выглядит сообщение об ошибке, и может ли сопровождающий +эффективно справиться с неудачей. + +Существуют некоторые методы для ясного воспроизведения тестовых входов и +выходов: + +* При выводе строковых данных [`%q` часто полезен](#use-percent-q) для + выделения важности значения и более легкого обнаружения плохих значений. +* При выводе (маленьких) структур `%+v` может быть полезнее, чем `%v`. +* Когда проверка больших значений завершается неудачей, [печать разницы + (diff)](#print-diffs) может облегчить понимание неудачи. + +<a id="print-diffs"></a> + +### Печать разниц (diffs) + +Если ваша функция возвращает большой вывод, читающему может быть трудно найти +различия, когда ваш тест проваливается. Вместо печати и возвращенного значения, +и ожидаемого значения создайте diff. + +Для вычисления разниц для таких значений предпочтительна `cmp.Diff`, особенно +для новых тестов и нового кода, но могут использоваться и другие инструменты. +См. [типы равенства] для рекомендаций относительно сильных и слабых сторон +каждой функции. + +* [`cmp.Diff`] + +* [`pretty.Compare`] + +Вы можете использовать пакет [`diff`] для сравнения многострочных строк или +списков строк. Вы можете использовать это как строительный блок для других видов +разниц. + +[типы равенства]: #types-of-equality +[`diff`]: https://pkg.go.dev/github.com/kylelemons/godebug/diff +[`pretty.Compare`]: + https://pkg.go.dev/github.com/kylelemons/godebug/pretty#Compare + +Добавьте некоторый текст в ваше сообщение об ошибке, объясняющий направление +diff. + +<!-- +The reversed order of want and got in these examples is intentional, as this is +the prevailing order across the Google codebase. The lack of a stance on which +order to use is also intentional, as there is no consensus which is +"most readable." + + +--> + +* Что-то вроде `diff (-want +got)` хорошо, когда вы используете пакеты `cmp`, + `pretty` и `diff` (если вы передаете `(want, got)` в функцию), потому что + `-` и `+`, которые вы добавляете в строку формата, будут соответствовать `-` + и `+`, которые фактически появляются в начале строк diff. Если вы передаете + `(got, want)` в вашу функцию, правильным ключом будет `(-got +want)` вместо + этого. + +* Пакет `messagediff` использует другой формат вывода, поэтому сообщение `diff + (want -> got)` уместно, когда вы его используете (если вы передаете `(want, + got)` в функцию), потому что направление стрелки будет соответствовать + направлению стрелки в строках "modified". + +Diff будет занимать несколько строк, поэтому вы должны напечатать новую строку +перед печатью diff. + +<a id="test-error-semantics"></a> + +### Тестирование семантики ошибок + +Когда модульный тест выполняет строковые сравнения или использует обычный `cmp` +для проверки того, что возвращаются определенные типы ошибок для определенных +входных данных, вы можете обнаружить, что ваши тесты становятся хрупкими, если +какое-либо из этих сообщений об ошибках будет переформулировано в будущем. +Поскольку это может превратить ваш модульный тест в детектор изменений (см. +[TotT: Change-Detector Tests Considered Harmful][tott-350] ), не используйте +строковое сравнение для проверки того, какой тип ошибки возвращает ваша функция. +Однако допустимо использовать строковые сравнения для проверки того, что +сообщения об ошибках, исходящие из тестируемого пакета, удовлетворяют +определенным свойствам, например, что оно включает имя параметра. + +Значения ошибок в Go обычно имеют компонент, предназначенный для человеческого +восприятия, и компонент, предназначенный для семантического управления потоком. +Тесты должны стремиться тестировать только семантическую информацию, которую +можно надежно наблюдать, а не отображаемую информацию, предназначенную для +отладки человеком, поскольку последняя часто подвержена будущим изменениям. +Рекомендации по созданию ошибок со смысловым значением см. [лучшие практики +относительно ошибок](https://neonxp.ru/pages/gostyleguide/google/best-practices/#error-handling). Если ошибка с недостаточной +семантической информацией исходит из зависимости вне вашего контроля, +рассмотрите возможность создания отчета об ошибке (bug) владельцу, чтобы помочь +улучшить API, а не полагаться на разбор сообщения об ошибке. + +В рамках модульных тестов часто важно только то, произошла ли ошибка или нет. +Если да, то достаточно проверить только, была ли ошибка ненулевой, когда вы +ожидали ошибки. Если вы хотите проверить, что ошибка семантически соответствует +какой-то другой ошибке, рассмотрите использование [`errors.Is`] или `cmp` с +[`cmpopts.EquateErrors`]. + +> **Примечание:** Если тест использует [`cmpopts.EquateErrors`], но все его +> значения `wantErr` либо `nil`, либо `cmpopts.AnyError`, то использование `cmp` +> является [ненужным механизмом](https://neonxp.ru/pages/gostyleguide/google/guide/#least-mechanism). Упростите код, сделав +> поле `want` типа `bool`. Затем вы можете использовать простое сравнение с +> `!=`. +> +> ```go +> // Хорошо: +> err := f(test.input) +> if gotErr := err != nil; gotErr != test.wantErr { +> t.Errorf("f(%q) = %v, want error presence = %v", test.input, err, test.wantErr) +> } +> ``` + +См. также [GoTip #13: Designing Errors for +Checking](https://google.github.io/styleguide/go/index.html#gotip). + +[tott-350]: + https://testing.googleblog.com/2015/01/testing-on-toilet-change-detector-tests.html +[`cmpopts.EquateErrors`]: + https://pkg.go.dev/github.com/google/go-cmp/cmp/cmpopts#EquateErrors +[`errors.Is`]: https://pkg.go.dev/errors#Is + +<a id="test-structure"></a> + +## Структура тестов + +<a id="subtests"></a> + +### Подтесты (Subtests) + +Стандартная библиотека тестирования Go предоставляет возможность [определять +подтесты]. Это позволяет гибкость в настройке и очистке, управлении +параллелизмом и фильтрации тестов. Подтесты могут быть полезны (особенно для +табличных тестов), но их использование не обязательно. См. также [пост в блоге +Go о подтестах](https://blog.golang.org/subtests). + +Подтесты не должны зависеть от выполнения других случаев для успеха или +начального состояния, поскольку ожидается, что подтесты могут быть запущены +индивидуально с использованием флагов `go test -run` или выражений [фильтра +тестов] Bazel. + +[определять подтесты]: + https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks +[фильтра тестов]: https://bazel.build/docs/user-manual#test-filter + +<a id="subtest-names"></a> + +#### Имена подтестов + +Назовите ваш подтест так, чтобы он был читаем в выводе теста и полезен в +командной строке для пользователей фильтрации тестов. Когда вы используете +`t.Run` для создания подтеста, первый аргумент используется как описательное имя +для теста. Чтобы гарантировать, что результаты тестов читаемы для людей, +читающих логи, выбирайте имена подтестов, которые останутся полезными и +читаемыми после экранирования. Думайте об именах подтестов скорее как об +идентификаторе функции, чем о прозаическом описании. + +Тестовый раннер заменяет пробелы подчеркиваниями и экранирует непечатаемые +символы. Чтобы обеспечить точную корреляцию между логами тестов и исходным +кодом, рекомендуется избегать использования этих символов в именах подтестов. + +Если ваши тестовые данные выигрывают от более длинного описания, рассмотрите +возможность поместить описание в отдельное поле (возможно, для печати с помощью +`t.Log` или рядом с сообщениями об ошибках). + +Подтесты могут быть запущены индивидуально с использованием флагов [Go test +runner] или Bazel [фильтра тестов], поэтому выбирайте описательные имена, +которые также легко набирать. + +> **Предупреждение:** Символы слеша особенно недружелюбны в именах подтестов, +> поскольку они имеют [особое значение для фильтров тестов]. +> +> > ```sh +> > # Плохо: +> > # Предполагая TestTime и t.Run("America/New_York", ...) +> > bazel test :mytest --test_filter="Time/New_York" # Ничего не запускает! +> > bazel test :mytest --test_filter="Time//New_York" # Правильно, но неудобно. +> > ``` + +Чтобы [идентифицировать входные данные] функции, включите их в сообщения об +ошибках теста, где они не будут экранированы тестовым раннером. + +```go +// Хорошо: +func TestTranslate(t *testing.T) { + data := []struct { + name, desc, srcLang, dstLang, srcText, wantDstText string + }{ + { + name: "hu=en_bug-1234", + desc: "regression test following bug 1234. contact: cleese", + srcLang: "hu", + srcText: "cigarettát és egy öngyújtót kérek", + dstLang: "en", + wantDstText: "cigarettes and a lighter please", + }, // ... + } + for _, d := range data { + t.Run(d.name, func(t *testing.T) { + got := Translate(d.srcLang, d.dstLang, d.srcText) + if got != d.wantDstText { + t.Errorf("%s\nTranslate(%q, %q, %q) = %q, want %q", + d.desc, d.srcLang, d.dstLang, d.srcText, got, d.wantDstText) + } + }) + } +} +``` + +Вот несколько примеров того, чего следует избегать: + +```go +// Плохо: +// Слишком многословно. +t.Run("check that there is no mention of scratched records or hovercrafts", ...) +// Слеши вызывают проблемы в командной строке. +t.Run("AM/PM confusion", ...) +``` + +См. также [Go Tip #117: Subtest +Names](https://google.github.io/styleguide/go/index.html#gotip). + +[Go test runner]: https://golang.org/cmd/go/#hdr-Testing_flags +[идентифицировать входные данные]: #identify-the-input +[особое значение для фильтров тестов]: + https://blog.golang.org/subtests#:~:text=Perhaps%20a%20bit,match%20any%20tests + +<a id="table-driven-tests"></a> + +### Табличные тесты + +Используйте табличные тесты, когда множество различных тестовых случаев могут +быть протестированы с помощью сходной тестовой логики. + +* При тестировании того, равен ли фактический вывод функции ожидаемому выводу. + Например, множество [тестов `fmt.Sprintf`] или минимальный фрагмент ниже. +* При тестировании того, соответствуют ли выводы функции всегда одному и тому + же набору инвариантов. Например, [тесты для `net.Dial`]. + +[тестов `fmt.Sprintf`]: + https://cs.opensource.google/go/go/+/master:src/fmt/fmt_test.go +[тестов для `net.Dial`]: + https://cs.opensource.google/go/go/+/master:src/net/dial_test.go;l=318;drc=5b606a9d2b7649532fe25794fa6b99bd24e7697c + +Вот минимальная структура табличного теста. При необходимости вы можете +использовать другие имена или добавить дополнительные средства, такие как +подтесты или функции настройки и очистки. Всегда помните о [полезных сообщениях +об ошибках тестов](#useful-test-failures). + +```go +// Хорошо: +func TestCompare(t *testing.T) { + compareTests := []struct { + a, b string + want int + }{ + {"", "", 0}, + {"a", "", 1}, + {"", "a", -1}, + {"abc", "abc", 0}, + {"ab", "abc", -1}, + {"abc", "ab", 1}, + {"x", "ab", 1}, + {"ab", "x", -1}, + {"x", "a", 1}, + {"b", "x", -1}, + // тестирование chunked-реализации runtime·memeq + {"abcdefgh", "abcdefgh", 0}, + {"abcdefghi", "abcdefghi", 0}, + {"abcdefghi", "abcdefghj", -1}, + } + + for _, test := range compareTests { + got := Compare(test.a, test.b) + if got != test.want { + t.Errorf("Compare(%q, %q) = %v, want %v", test.a, test.b, got, test.want) + } + } +} +``` + +**Примечание**: Сообщения об ошибках в приведенном выше примере соответствуют +рекомендациям [идентифицировать функцию](#identify-the-function) и +[идентифицировать входные данные](#identify-the-input). Нет необходимости +[идентифицировать строку численно](#table-tests-identifying-the-row). + +Когда некоторые тестовые случаи нужно проверять с помощью логики, отличной от +других тестовых случаев, уместно написать несколько тестовых функций, как +объясняется в [GoTip #50: Disjoint Table Tests]. + +Когда дополнительные тестовые случаи просты (например, базовая проверка ошибок) +и не вводят условный поток кода в теле цикла табличного теста, допустимо +включить этот случай в существующий тест, хотя будьте осторожны, используя такую +логику. То, что начинается просто сегодня, может органически вырасти во что-то +неподдерживаемое. + +Например: + +```go +func TestDivide(t *testing.T) { + tests := []struct { + dividend, divisor int + want int + wantErr bool + }{ + { + dividend: 4, + divisor: 2, + want: 2, + }, + { + dividend: 10, + divisor: 2, + want: 5, + }, + { + dividend: 1, + divisor: 0, + wantErr: true, + }, + } + + for _, test := range tests { + got, err := Divide(test.dividend, test.divisor) + if (err != nil) != test.wantErr { + t.Errorf("Divide(%d, %d) error = %v, want error presence = %t", test.dividend, test.divisor, err, test.wantErr) + } + + // В этом примере мы тестируем значение результата только когда тестируемая функция не завершилась неудачей. + if err != nil { + continue + } + + if got != test.want { + t.Errorf("Divide(%d, %d) = %d, want %d", test.dividend, test.divisor, got, test.want) + } + } +} +``` + +Более сложная логика в вашем тестовом коде, например сложная проверка ошибок на +основе условных различий в настройке теста (часто основанных на входных +параметрах табличного теста), может быть [трудной для +понимания](https://neonxp.ru/pages/gostyleguide/google/guide/#maintainability), когда каждая запись в таблице имеет +специализированную логику на основе входных данных. Если тестовые случаи имеют +разную логику, но идентичную настройку, последовательность +[подтестов](#subtests) внутри одной тестовой функции может быть более читаемой. +Тестовый помощник также может быть полезен для упрощения настройки теста с целью +сохранения читаемости тела теста. + +Вы можете комбинировать табличные тесты с несколькими тестовыми функциями. +Например, при тестировании того, что вывод функции точно соответствует +ожидаемому выводу и что функция возвращает ненулевую ошибку для неверного ввода, +лучшим подходом является написание двух отдельных табличных тестовых функций: +одну для обычных не-ошибочных выводов, и одну для ошибочных выводов. + +[GoTip #50: Disjoint Table Tests]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="table-tests-data-driven"></a> + +#### Тестовые случаи на основе данных + +Строки табличных тестов иногда могут становиться сложными, причем значения строк +диктуют условное поведение внутри тестового случая. Дополнительная ясность от +дублирования между тестовыми случаями необходима для читаемости. + +```go +// Хорошо: +type decodeCase struct { + name string + input string + output string + err error +} + +func TestDecode(t *testing.T) { + // setupCodex медленный, так как создает реальный Codex для теста. + codex := setupCodex(t) + + var tests []decodeCase // строки опущены для краткости + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + output, err := Decode(test.input, codex) + if got, want := output, test.output; got != want { + t.Errorf("Decode(%q) = %v, want %v", test.input, got, want) + } + if got, want := err, test.err; !cmp.Equal(got, want) { + t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) + } + }) + } +} + +func TestDecodeWithFake(t *testing.T) { + // fakeCodex — это быстрое приближение реального Codex. + codex := newFakeCodex() + + var tests []decodeCase // строки опущены для краткости + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + output, err := Decode(test.input, codex) + if got, want := output, test.output; got != want { + t.Errorf("Decode(%q) = %v, want %v", test.input, got, want) + } + if got, want := err, test.err; !cmp.Equal(got, want) { + t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) + } + }) + } +} +``` + +В контринте ниже обратите внимание, как сложно различить, какой тип `Codex` +используется на тестовый случай в настройке случая. (Выделенные части нарушают +совет из [TotT: Data Driven Traps!][tott-97] .) + +```go +// Плохо: +type decodeCase struct { + name string + input string + codex testCodex + output string + err error +} + +type testCodex int + +const ( + fake testCodex = iota + prod +) + +func TestDecode(t *testing.T) { + var tests []decodeCase // строки опущены для краткости + + for _, test := tests { + t.Run(test.name, func(t *testing.T) { + var codex Codex + switch test.codex { + case fake: + codex = newFakeCodex() + case prod: + codex = setupCodex(t) + default: + t.Fatalf("Unknown codex type: %v", codex) + } + output, err := Decode(test.input, codex) + if got, want := output, test.output; got != want { + t.Errorf("Decode(%q) = %q, want %q", test.input, got, want) + } + if got, want := err, test.err; !cmp.Equal(got, want) { + t.Errorf("Decode(%q) err %q, want %q", test.input, got, want) + } + }) + } +} +``` + +[tott-97]: https://testing.googleblog.com/2008/09/tott-data-driven-traps.html + +<a id="table-tests-identifying-the-row"></a> + +#### Идентификация строки + +Не используйте индекс теста в тестовой таблице в качестве замены именования +ваших тестов или печати входных данных. Никто не хочет проходить через вашу +тестовую таблицу и считать записи, чтобы выяснить, какой тестовый случай +провалился. + +```go +// Плохо: +tests := []struct { + input, want string +}{ + {"hello", "HELLO"}, + {"wORld", "WORLD"}, +} +for i, d := range tests { + if strings.ToUpper(d.input) != d.want { + t.Errorf("Failed on case #%d", i) + } +} +``` + +Добавьте описание теста в вашу тестовую структуру и печатайте его вместе с +сообщениями об ошибках. При использовании подтестов имя вашего подтеста должно +эффективно идентифицировать строку. + +**Важно:** Хотя `t.Run` ограничивает область вывода и выполнения, вы должны +всегда [идентифицировать входные данные]. Имена строк табличных тестов должны +следовать [руководству по именованию подтестов]. + +[идентифицировать входные данные]: #identify-the-input +[руководству по именованию подтестов]: #subtest-names + +<a id="mark-test-helpers"></a> + +### Тестовые помощники (Test helpers) + +Тестовый помощник — это функция, выполняющая задачу настройки или очистки. Все +ошибки, возникающие в тестовых помощниках, должны быть ошибками окружения (а не +кода под тестом) — например, когда тестовая база данных не может быть запущена +потому что на этой машине не осталось свободных портов. + +Если вы передаете `*testing.T`, вызовите [`t.Helper`], чтобы приписать ошибки в +тестовом помощнике строке, где помощник вызывается. Этот параметр должен идти +после параметра [контекста](#contexts), если он присутствует, и перед любыми +оставшимися параметрами. + +```go +// Хорошо: +func TestSomeFunction(t *testing.T) { + golden := readFile(t, "testdata/golden-result.txt") + // ... тесты против golden ... +} + +// readFile возвращает содержимое файла данных. +// Может вызываться только из той же горутины, которая начала тест. +func readFile(t *testing.T, filename string) string { + t.Helper() + contents, err := runfiles.ReadFile(filename) + if err != nil { + t.Fatal(err) + } + return string(contents) +} +``` + +Не используйте этот шаблон, когда он скрывает связь между неудачей теста и +условиями, которые к ней привели. Конкретно, рекомендации о [библиотеках +утверждений](#assert) все еще применяются, и [`t.Helper`] не должен +использоваться для реализации таких библиотек. + +**Совет:** Подробнее о различии между тестовыми помощниками и помощниками +утверждений см. [лучшие практики](https://neonxp.ru/pages/gostyleguide/google/best-practices/#test-functions). + +Хотя вышесказанное относится к `*testing.T`, большая часть рекомендаций остается +той же для вспомогательных функций бенчмарков и фаззинга. + +[`t.Helper`]: https://pkg.go.dev/testing#T.Helper + +<a id="test-package"></a> + +### Тестовый пакет + +<a id="TOC-TestPackage"></a> + +<a id="test-same-package"></a> + +#### Тесты в том же пакете + +Тесты могут быть определены в том же пакете, что и тестируемый код. + +Чтобы написать тест в том же пакете: + +* Поместите тесты в файл `foo_test.go` +* Используйте `package foo` для тестового файла +* Не импортируйте явно тестируемый пакет + +```build +# Хорошо: +go_library( + name = "foo", + srcs = ["foo.go"], + deps = [ + ... + ], +) + +go_test( + name = "foo_test", + size = "small", + srcs = ["foo_test.go"], + library = ":foo", + deps = [ + ... + ], +) +``` + +Тест в том же пакете может обращаться к неэкспортированным идентификаторам в +пакете. Это может обеспечить лучшее покрытие тестами и более лаконичные тесты. +Имейте в виду, что любые [примеры], объявленные в тесте, не будут иметь имен +пакетов, которые понадобятся пользователю в их коде. + +[`library`]: + https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_library +[примеры]: #examples + +<a id="test-different-package"></a> + +#### Тесты в другом пакете + +Не всегда уместно или даже возможно определить тест в том же пакете, что и +тестируемый код. В этих случаях используйте имя пакета с суффиксом `_test`. Это +исключение из правила "без подчеркиваний" для [имен пакетов](#package-names). +Например: + +* Если интеграционный тест не имеет очевидной библиотеки, к которой он + принадлежит + + ```go + // Хорошо: + package gmailintegration_test + + import "testing" + ``` + +* Если определение тестов в том же пакете приводит к циклическим зависимостям + + ```go + // Хорошо: + package fireworks_test + + import ( + "fireworks" + "fireworkstestutil" // fireworkstestutil также импортирует fireworks + ) + ``` + +<a id="use-package-testing"></a> + +### Использование пакета `testing` + +Стандартная библиотека Go предоставляет пакет [`testing`]. Это единственный +фреймворк тестирования, разрешенный для кода Go в кодовой базе Google. В +частности, [библиотеки утверждений](#assert) и сторонние фреймворки тестирования +не разрешены. + +Пакет `testing` предоставляет минимальный, но полный набор функциональности для +написания хороших тестов: + +* Тесты верхнего уровня +* Бенчмарки +* [Исполняемые примеры](https://blog.golang.org/examples) +* Подтесты +* Логирование +* Ошибки и фатальные ошибки + +Они разработаны для слаженной работы с основными языковыми возможностями, такими +как [составные литералы] и [if с инициализатором], чтобы позволить авторам +тестов писать [понятные, читаемые и поддерживаемые тесты]. + +[`testing` package]: https://pkg.go.dev/testing +[составные литералы]: https://go.dev/ref/spec#Composite_literals +[if с инициализатором]: https://go.dev/ref/spec#If_statements + +<a id="non-decisions"></a> + +## Не-решения + +Руководство по стилю не может перечислять позитивные предписания по всем +вопросам, как не может оно перечислить все вопросы, по которым оно не дает +мнения. Тем не менее, вот несколько вещей, по которым сообщество читаемости +ранее спорило и не достигло консенсуса. + +* **Локальная инициализация переменных нулевым значением**. `var i int` и `i + := 0` эквивалентны. См. также [лучшие практики инициализации]. +* **Пустой составной литерал vs. `new` или `make`**. `&File{}` и `new(File)` + эквивалентны. Так же `map[string]bool{}` и `make(map[string]bool)`. См. + также [лучшие практики составных объявлений]. +* **Порядок аргументов got, want в вызовах cmp.Diff**. Будьте локально + последовательны и [включите легенду](#print-diffs) в ваше сообщение об + ошибке. +* **`errors.New` vs `fmt.Errorf` на неформатированных строках**. + `errors.New("foo")` и `fmt.Errorf("foo")` могут использоваться + взаимозаменяемо. + +Если возникнут особые обстоятельства, наставник по читаемости может сделать +необязательный комментарий, но в целом автор волен выбирать предпочитаемый им +стиль в данной ситуации. + +Естественно, если что-то, не охваченное руководством по стилю, требует +дополнительного обсуждения, авторы могут спросить — либо в конкретном ревью, +либо на внутренних досках сообщений. + +[лучшие практики составных объявлений]: + https://google.github.io/styleguide/go/best-practices#vardeclcomposite +[лучшие практики инициализации]: + https://google.github.io/styleguide/go/best-practices#vardeclinitialization |
