diff options
| author | 2026-01-31 20:38:50 +0300 | |
|---|---|---|
| committer | 2026-01-31 23:38:53 +0300 | |
| commit | 49458f5ffd5a48c465117ec27f6437683f75acc1 (patch) | |
| tree | a99ee68116d10c2b2e5a70c442cdadec95ba793c /content/pages/gostyleguide/google/best-practices.md | |
| download | blog-49458f5ffd5a48c465117ec27f6437683f75acc1.tar.gz blog-49458f5ffd5a48c465117ec27f6437683f75acc1.tar.bz2 blog-49458f5ffd5a48c465117ec27f6437683f75acc1.tar.xz blog-49458f5ffd5a48c465117ec27f6437683f75acc1.zip | |
initial
Diffstat (limited to 'content/pages/gostyleguide/google/best-practices.md')
| -rw-r--r-- | content/pages/gostyleguide/google/best-practices.md | 3727 |
1 files changed, 3727 insertions, 0 deletions
diff --git a/content/pages/gostyleguide/google/best-practices.md b/content/pages/gostyleguide/google/best-practices.md new file mode 100644 index 0000000..4fc59b1 --- /dev/null +++ b/content/pages/gostyleguide/google/best-practices.md @@ -0,0 +1,3727 @@ +--- +order: 1 +title: Google Go Style Guide — Лучшие практики +--- + +# Лучшие практики стиля Go (Go Style Best Practices) + +Оригинал: https://google.github.io/styleguide/go/best-practices + +[Обзор](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 (Go +Style)](https://neonxp.ru/pages/gostyleguide/google/) в Google. Данный документ **не является ни [нормативным +(normative)](https://neonxp.ru/pages/gostyleguide/google/#normative), ни [каноническим (canonical)](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> + +## О документе (About) + +В этом документе представлены **рекомендации о том, как наилучшим образом +применять Руководство по стилю Go**. Эти рекомендации предназначены для типичных +ситуаций, возникающих часто, но могут не применяться в каждом случае. По +возможности обсуждаются несколько альтернативных подходов, а также соображения, +которые учитываются при решении о том, когда их применять, а когда нет. + +См. [обзор](https://neonxp.ru/pages/gostyleguide/google/#about) для полного набора документов руководства по стилю. + +<a id="naming"></a> + +## Именование (Naming) + +<a id="function-names"></a> + +### Имена функций и методов + +<a id="function-name-repetition"></a> + +#### Избегайте повторения (Avoid repetition) + +При выборе имени для функции или метода учитывайте контекст, в котором это имя +будет прочитано. Рассмотрите следующие рекомендации, чтобы избежать избыточного +[повторения (repetition)](https://neonxp.ru/pages/gostyleguide/google/decisions/#repetition) в месте вызова (call site): + +* Следующее, как правило, можно опустить в именах функций и методов: + + * Типы входных и выходных данных (если нет конфликта) + * Тип получателя (receiver) метода + * Является ли входной или выходной параметр указателем (pointer) + +* Для функций не следует [повторять имя + пакета](https://neonxp.ru/pages/gostyleguide/google/decisions/#repetitive-with-package). + + ```go + // Плохо: + package yamlconfig + + func ParseYAMLConfig(input string) (*Config, error) + ``` + + ```go + // Хорошо: + package yamlconfig + + func Parse(input string) (*Config, error) + ``` + +* Для методов не следует повторять имя получателя метода. + + ```go + // Плохо: + func (c *Config) WriteConfigTo(w io.Writer) (int64, error) + ``` + + ```go + // Хорошо: + func (c *Config) WriteTo(w io.Writer) (int64, error) + ``` + +* Не повторяйте имена переменных, передаваемых в качестве параметров. + + ```go + // Плохо: + func OverrideFirstWithSecond(dest, source *Config) error + ``` + + ```go + // Хорошо: + func Override(dest, source *Config) error + ``` + +* Не повторяйте имена и типы возвращаемых значений. + + ```go + // Плохо: + func TransformToJSON(input *Config) *jsonconfig.Config + ``` + + ```go + // Хорошо: + func Transform(input *Config) *jsonconfig.Config + ``` + +Когда необходимо устранить неоднозначность между функциями с похожими именами, +допустимо включить дополнительную информацию. + +```go +// Хорошо: +func (c *Config) WriteTextTo(w io.Writer) (int64, error) +func (c *Config) WriteBinaryTo(w io.Writer) (int64, error) +``` + +<a id="function-name-conventions"></a> + +#### Соглашения об именовании (Naming conventions) + +Существуют и другие общие соглашения при выборе имен для функций и методов: + +* Функции, которые что-то возвращают, получают имена, похожие на + существительные. + + ```go + // Хорошо: + func (c *Config) JobName(key string) (value string, ok bool) + ``` + + Следствием этого является то, что имена функций и методов должны [избегать + префикса `Get`](https://neonxp.ru/pages/gostyleguide/google/decisions/#getters). + + ```go + // Плохо: + func (c *Config) GetJobName(key string) (value string, ok bool) + ``` + +* Функции, которые что-то делают, получают имена, похожие на глаголы. + + ```go + // Хорошо: + func (c *Config) WriteDetail(w io.Writer) (int64, error) + ``` + +* Идентичные функции, которые отличаются только типами, включают имя типа в + конце имени. + + ```go + // Хорошо: + func ParseInt(input string) (int, error) + func ParseInt64(input string) (int64, error) + func AppendInt(buf []byte, value int) []byte + func AppendInt64(buf []byte, value int64) []byte + ``` + + Если существует ясная "основная" версия, тип может быть опущен в имени для + этой версии: + + ```go + // Хорошо: + func (c *Config) Marshal() ([]byte, error) + func (c *Config) MarshalText() (string, error) + ``` + +<a id="naming-doubles"></a> + +### Тестовые дубли (Test doubles) и вспомогательные пакеты (helper packages) + +Существует несколько подходов, которые можно применить для [именования] пакетов +и типов, которые предоставляют тестовые вспомогательные средства и особенно +[тестовые дубли (test doubles)]. Тестовым дублем может быть заглушка (stub), +фейк (fake), мок (mock) или шпион (spy). + +Эти примеры в основном используют заглушки. Обновите свои имена соответствующим +образом, если ваш код использует фейки или другой вид тестового дубля. + +[именование]: guide#naming +[тестовые дубли (test doubles)]: + https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts + +Предположим, у вас есть хорошо сфокусированный пакет, предоставляющий +production-код, подобный этому: + +```go +package creditcard + +import ( + "errors" + + "path/to/money" +) + +// ErrDeclined указывает, что эмитент отклонил операцию. +var ErrDeclined = errors.New("creditcard: declined") + +// Card содержит информацию о кредитной карте, такую как эмитент, +// срок действия и лимит. +type Card struct { + // опущено +} + +// Service позволяет выполнять операции с кредитными картами через внешние +// процессинговые системы, такие как списание, авторизация, возврат средств и подписка. +type Service struct { + // опущено +} + +func (s *Service) Charge(c *Card, amount money.Money) error { /* опущено */ } +``` + +<a id="naming-doubles-helper-package"></a> + +#### Создание вспомогательных тестовых пакетов (Creating test helper packages) + +Предположим, вы хотите создать пакет, содержащий тестовые дубли для другого +пакета. Воспользуемся `package creditcard` (из примера выше): + +Один из подходов — создать новый Go-пакет на основе production-пакета для +тестирования. Безопасный выбор — добавить слово `test` к оригинальному имени +пакета ("creditcard" + "test"): + +```go +// Хорошо: +package creditcardtest +``` + +Если явно не указано иное, все примеры в следующих разделах находятся в `package +creditcardtest`. + +<a id="naming-doubles-simple"></a> + +#### Простой случай (Simple case) + +Вы хотите добавить набор тестовых дублей для `Service`. Поскольку `Card` по сути +является простым типом данных, похожим на сообщение Protocol Buffer, он не +требует специальной обработки в тестах, поэтому дубль не нужен. Если вы ожидаете +только тестовые дубли для одного типа (например, `Service`), вы можете +использовать лаконичный подход к именованию дублей: + +```go +// Хорошо: +import ( + "path/to/creditcard" + "path/to/money" +) + +// Stub заглушает creditcard.Service и не предоставляет собственного поведения. +type Stub struct{} + +func (Stub) Charge(*creditcard.Card, money.Money) error { return nil } +``` + +Это строго предпочтительнее, чем выбор имен типа `StubService` или очень плохого +`StubCreditCardService`, потому что базовое имя пакета и его доменные типы +подразумевают, что такое `creditcardtest.Stub`. + +Наконец, если пакет собирается с помощью Bazel, убедитесь, что новое правило +`go_library` для пакета помечено как `testonly`: + +```build +# Хорошо: +go_library( + name = "creditcardtest", + srcs = ["creditcardtest.go"], + deps = [ + ":creditcard", + ":money", + ], + testonly = True, +) +``` + +Приведенный выше подход является общепринятым и будет достаточно хорошо понят +другими инженерами. + +См. также: + +* [Go Tip #42: Authoring a Stub for + Testing](https://google.github.io/styleguide/go/index.html#gotip) + +<a id="naming-doubles-multiple-behaviors"></a> + +#### Несколько вариантов поведения тестового дубля (Multiple test double behaviors) + +Когда одного вида заглушки недостаточно (например, нужна еще одна, которая +всегда завершается ошибкой), мы рекомендуем называть заглушки в соответствии с +поведением, которое они эмулируют. Здесь мы переименовываем `Stub` в +`AlwaysCharges` и вводим новую заглушку `AlwaysDeclines`: + +```go +// Хорошо: +// AlwaysCharges заглушает creditcard.Service и имитирует успех. +type AlwaysCharges struct{} + +func (AlwaysCharges) Charge(*creditcard.Card, money.Money) error { return nil } + +// AlwaysDeclines заглушает creditcard.Service и имитирует отклоненные операции. +type AlwaysDeclines struct{} + +func (AlwaysDeclines) Charge(*creditcard.Card, money.Money) error { + return creditcard.ErrDeclined +} +``` + +<a id="naming-doubles-multiple-types"></a> + +#### Несколько дублей для нескольких типов (Multiple doubles for multiple types) + +Но теперь предположим, что `package creditcard` содержит несколько типов, для +которых стоит создавать дубли, как показано ниже с `Service` и `StoredValue`: + +```go +package creditcard + +type Service struct { + // опущено +} + +type Card struct { + // опущено +} + +// StoredValue управляет кредитными балансами клиентов. Это применяется, когда +// возвращенный товар зачисляется на локальный счет клиента, а не обрабатывается +// эмитентом кредитной карты. По этой причине он реализован как отдельный сервис. +type StoredValue struct { + // опущено +} + +func (s *StoredValue) Credit(c *Card, amount money.Money) error { /* опущено */ } +``` + +В этом случае более явное именование тестовых дублей имеет смысл: + +```go +// Хорошо: +type StubService struct{} + +func (StubService) Charge(*creditcard.Card, money.Money) error { return nil } + +type StubStoredValue struct{} + +func (StubStoredValue) Credit(*creditcard.Card, money.Money) error { return nil } +``` + +<a id="naming-doubles-local-variables"></a> + +#### Локальные переменные в тестах (Local variables in tests) + +Когда переменные в ваших тестах ссылаются на дубли, выберите имя, которое +наиболее четко отличает дубль от других production-типов, исходя из контекста. +Рассмотрим некоторый production-код, который вы хотите протестировать: + +```go +package payment + +import ( + "path/to/creditcard" + "path/to/money" +) + +type CreditCard interface { + Charge(*creditcard.Card, money.Money) error +} + +type Processor struct { + CC CreditCard +} + +var ErrBadInstrument = errors.New("payment: instrument is invalid or expired") + +func (p *Processor) Process(c *creditcard.Card, amount money.Money) error { + if c.Expired() { + return ErrBadInstrument + } + return p.CC.Charge(c, amount) +} +``` + +В тестах тестовой дубль типа "шпион" (spy) для `CreditCard` противопоставляется +production-типам, поэтому добавление префикса к имени может улучшить ясность. + +```go +// Хорошо: +package payment + +import "path/to/creditcardtest" + +func TestProcessor(t *testing.T) { + var spyCC creditcardtest.Spy + proc := &Processor{CC: spyCC} + + // объявления опущены: card и amount + if err := proc.Process(card, amount); err != nil { + t.Errorf("proc.Process(card, amount) = %v, want nil", err) + } + + charges := []creditcardtest.Charge{ + {Card: card, Amount: amount}, + } + + if got, want := spyCC.Charges, charges; !cmp.Equal(got, want) { + t.Errorf("spyCC.Charges = %v, want %v", got, want) + } +} +``` + +Это понятнее, чем когда имя не имеет префикса. + +```go +// Плохо: +package payment + +import "path/to/creditcardtest" + +func TestProcessor(t *testing.T) { + var cc creditcardtest.Spy + + proc := &Processor{CC: cc} + + // объявления опущены: card и amount + if err := proc.Process(card, amount); err != nil { + t.Errorf("proc.Process(card, amount) = %v, want nil", err) + } + + charges := []creditcardtest.Charge{ + {Card: card, Amount: amount}, + } + + if got, want := cc.Charges, charges; !cmp.Equal(got, want) { + t.Errorf("cc.Charges = %v, want %v", got, want) + } +} +``` + +<a id="shadowing"></a> + +### Затенение (Shadowing) + +**Примечание:** Это объяснение использует два неформальных термина, *stomping* и +*shadowing*. Они не являются официальными концепциями в спецификации языка Go. + +Как и во многих языках программирования, в Go есть изменяемые переменные: +присваивание переменной меняет ее значение. + +```go +// Хорошо: +func abs(i int) int { + if i < 0 { + i *= -1 + } + return i +} +``` + +При использовании [короткого объявления переменных (short variable +declarations)] с оператором `:=` в некоторых случаях новая переменная не +создается. Мы можем назвать это *stomping* (затирание). Это допустимо, когда +исходное значение больше не нужно. + +```go +// Хорошо: +// innerHandler — это вспомогательная функция для некоторого обработчика запросов, который сам +// выполняет запросы к другим бэкендам. +func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { + // Безусловно ограничиваем срок действия (deadline) для этой части обработки запроса. + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + ctxlog.Info(ctx, "Capped deadline in inner request") + + // Код здесь больше не имеет доступа к оригинальному контексту. + // Это хороший стиль, если при первом написании вы предполагаете, + // что даже по мере роста кода ни одна операция законно не должна + // использовать (возможно, неограниченный) оригинальный контекст, предоставленный вызывающей стороной. + + // ... +} +``` + +Однако будьте осторожны с использованием короткого объявления переменных в новой +области видимости: это вводит новую переменную. Мы можем назвать это *shadowing* +(затенение) исходной переменной. Код после конца блока ссылается на оригинал. +Вот ошибочная попытка условно сократить срок действия (deadline): + +```go +// Плохо: +func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { + // Попытка условно ограничить срок действия. + if *shortenDeadlines { + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + ctxlog.Info(ctx, "Capped deadline in inner request") + } + + // ОШИБКА: "ctx" здесь снова означает контекст, предоставленный вызывающей стороной. + // Вышеуказанный ошибочный код скомпилировался, потому что и ctx, и cancel + // использовались внутри оператора if. + + // ... +} +``` + +Правильная версия кода может быть такой: + +```go +// Хорошо: +func (s *Server) innerHandler(ctx context.Context, req *pb.MyRequest) *pb.MyResponse { + if *shortenDeadlines { + var cancel func() + // Обратите внимание на использование простого присваивания, =, а не :=. + ctx, cancel = context.WithTimeout(ctx, 3*time.Second) + defer cancel() + ctxlog.Info(ctx, "Capped deadline in inner request") + } + // ... +} +``` + +В случае, который мы назвали stomping, поскольку нет новой переменной, тип +присваиваемого значения должен совпадать с типом исходной переменной. При +затенении вводится совершенно новая сущность, поэтому она может иметь другой +тип. Намеренное затенение может быть полезной практикой, но вы всегда можете +использовать новое имя, если это улучшает [ясность (clarity)](https://neonxp.ru/pages/gostyleguide/google/guide/#clarity). + +Не рекомендуется использовать переменные с теми же именами, что и у стандартных +пакетов, за исключением очень маленьких областей видимости, потому что это +делает функции и значения из этого пакета недоступными. И наоборот, при выборе +имени для вашего пакета избегайте имен, которые, вероятно, потребуют +[переименования импорта (import renaming)](https://neonxp.ru/pages/gostyleguide/google/decisions/#import-renaming) или +вызовут затенение иначе хороших имен переменных на стороне клиента. + +```go +// Плохо: +func LongFunction() { + url := "https://example.com/" + // Упс, теперь мы не можем использовать net/url в коде ниже. +} +``` + +[короткого объявления переменных (short variable declarations)]: + https://go.dev/ref/spec#Short_variable_declarations + +<a id="util-packages"></a> + +### Пакеты `util` (Util packages) + +Пакеты Go имеют имя, указанное в объявлении `package`, отдельное от пути +импорта. Имя пакета имеет большее значение для читаемости, чем путь. + +Имена пакетов Go должны быть [связаны с тем, что предоставляет +пакет](https://neonxp.ru/pages/gostyleguide/google/decisions/#package-names). Называть пакет просто `util`, `helper`, +`common` или подобным обычно плохой выбор (хотя это может быть использовано как +*часть* имени). Неинформативные имена затрудняют чтение кода, и если они +используются слишком широко, они могут вызывать ненужные [конфликты +импорта](https://neonxp.ru/pages/gostyleguide/google/decisions/#import-renaming). + +Вместо этого подумайте, как будет выглядеть место вызова (callsite). + +```go +// Хорошо: +db := spannertest.NewDatabaseFromFile(...) + +_, err := f.Seek(0, io.SeekStart) + +b := elliptic.Marshal(curve, x, y) +``` + +Вы можете примерно понять, что делает каждая из этих строк, даже не зная списка +импортов (`cloud.google.com/go/spanner/spannertest`, `io` и `crypto/elliptic`). +С менее сфокусированными именами они могли бы читаться так: + +```go +// Плохо: +db := test.NewDatabaseFromFile(...) + +_, err := f.Seek(0, common.SeekStart) + +b := helper.Marshal(curve, x, y) +``` + +<a id="package-size"></a> + +## Размер пакета (Package size) + +Если вы задаетесь вопросом, насколько большими должны быть ваши пакеты Go и +следует ли помещать связанные типы в один пакет или разделять их на разные, +хорошим началом будет [пост в блоге Go об именах пакетов][blog-pkg-names]. +Несмотря на название поста, он не только об именовании. Он содержит полезные +подсказки и ссылается на несколько полезных статей и докладов. + +Вот некоторые другие соображения и примечания. + +Пользователи видят [godoc] для пакета на одной странице, и любые +экспортированные методы типов, предоставляемых пакетом, группируются по их типу. +Godoc также группирует конструкторы вместе с типами, которые они возвращают. +Если *клиентскому коду* (client code) вероятно потребуется, чтобы два значения +разных типов взаимодействовали друг с другом, может быть удобно для пользователя +иметь их в одном пакете. + +Код внутри пакета имеет доступ к неэкспортированным идентификаторам пакета. Если +у вас есть несколько связанных типов, *реализация* которых тесно связана, +размещение их в одном пакете позволяет достичь этой связи без загрязнения +публичного API этими деталями. Хороший тест для этой связи — представить +гипотетического пользователя двух пакетов, где пакеты охватывают тесно связанные +темы: если пользователь должен импортировать оба пакета, чтобы использовать +любой из них хоть сколько-нибудь значимо, обычно правильным решением будет +объединить их вместе. Стандартная библиотека в целом хорошо демонстрирует такую +область видимости (scoping) и слоистость (layering). + +При всем сказанном, помещение всего вашего проекта в один пакет, вероятно, +сделает этот пакет слишком большим. Когда что-то концептуально отличается, +предоставление ему собственного небольшого пакета может облегчить его +использование. Короткое имя пакета, известное клиентам, вместе с именем +экспортированного типа работают вместе, чтобы создать значимый идентификатор: +например, `bytes.Buffer`, `ring.New`. [Пост об именах пакетов][blog-pkg-names] +содержит больше примеров. + +Стиль Go гибок относительно размера файлов, потому что сопровождающие могут +перемещать код внутри пакета из одного файла в другой, не влияя на вызывающую +сторону. Но в качестве общего руководства: обычно не стоит иметь один файл с +тысячами строк или множество крошечных файлов. Нет такого соглашения, как "один +тип — один файл", как в некоторых других языках. Эмпирическое правило: файлы +должны быть достаточно сфокусированными, чтобы сопровождающий мог определить, в +каком файле что-то находится, и достаточно маленькими, чтобы было легко найти +это, когда вы там окажетесь. Стандартная библиотека часто разделяет большие +пакеты на несколько исходных файлов, группируя связанный код по файлам. Исходный +код [пакета `bytes`] является хорошим примером. Пакеты с длинной документацией +могут выбрать выделение одного файла с именем `doc.go`, который содержит +[документацию пакета](https://neonxp.ru/pages/gostyleguide/google/decisions/#package-comments), объявление пакета и больше +ничего, но это не обязательно. + +Внутри кодовой базы Google и в проектах, использующих Bazel, структура каталогов +для кода Go отличается от таковой в open source проектах на Go: вы можете иметь +несколько целей `go_library` в одном каталоге. Хорошей причиной для выделения +каждому пакету собственного каталога является ожидание открытия исходного кода +вашего проекта в будущем. + +Несколько неканонических справочных примеров, чтобы помочь продемонстрировать +эти идеи на практике: + +* маленькие пакеты, содержащие одну связную идею, которая не требует + добавления или удаления чего-либо еще: + + * [пакет `csv`][package `csv`]: кодирование и декодирование данных CSV с + разделением ответственности соответственно между [reader.go] и + [writer.go]. + * [пакет `expvar`][package `expvar`]: "белый ящик" (whitebox) телеметрии + программы, полностью содержащийся в [expvar.go]. + +* пакеты умеренного размера, содержащие одну большую предметную область и + несколько связанных с ней ответственностей: + + * [пакет `flag`][package `flag`]: управление флагами командной строки, + полностью содержащееся в [flag.go]. + +* большие пакеты, которые разделяют несколько тесно связанных предметных + областей по нескольким файлам: + + * [пакет `http`][package `http`]: ядро HTTP: [client.go][http-client], + поддержка HTTP-клиентов; [server.go][http-server], поддержка + HTTP-серверов; [cookie.go], управление куками. + * [пакет `os`][package `os`]: кроссплатформенные абстракции операционной + системы: [exec.go], управление подпроцессами; [file.go], управление + файлами; [tempfile.go], временные файлы. + +См. также: + +* [Пакеты тестовых дублей (Test double packages)](#naming-doubles) +* [Organizing Go Code (Blog Post)] +* [Organizing Go Code (Presentation)] + +[blog-pkg-names]: https://go.dev/blog/package-names +[пакет `bytes`]: https://go.dev/src/bytes/ +[Organizing Go Code (Blog Post)]: https://go.dev/blog/organizing-go-code +[Organizing Go Code (Presentation)]: https://go.dev/talks/2014/organizeio.slide +[пакет `csv`]: https://pkg.go.dev/encoding/csv +[reader.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/encoding/csv/reader.go +[writer.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/encoding/csv/writer.go +[пакет `expvar`]: https://pkg.go.dev/expvar +[expvar.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/expvar/expvar.go +[пакет `flag`]: https://pkg.go.dev/flag +[flag.go]: https://go.googlesource.com/go/+/refs/heads/master/src/flag/flag.go +[godoc]: https://pkg.go.dev/ +[пакет `http`]: https://pkg.go.dev/net/http +[http-client]: + https://go.googlesource.com/go/+/refs/heads/master/src/net/http/client.go +[http-server]: + https://go.googlesource.com/go/+/refs/heads/master/src/net/http/server.go +[cookie.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/net/http/cookie.go +[пакет `os`]: https://pkg.go.dev/os +[exec.go]: https://go.googlesource.com/go/+/refs/heads/master/src/os/exec.go +[file.go]: https://go.googlesource.com/go/+/refs/heads/master/src/os/file.go +[tempfile.go]: + https://go.googlesource.com/go/+/refs/heads/master/src/os/tempfile.go + +<a id="imports"></a> + +## Импорт (Imports) + +<a id="import-protos"></a> + +### Сообщения Protocol Buffer и заглушки (Stubs) + +Импорты библиотек proto обрабатываются иначе, чем стандартные импорты Go, из-за +их межъязыковой природы. Соглашение для переименованных импортов proto основано +на правиле, которое сгенерировало пакет: + +* Суффикс `pb` обычно используется для правил `go_proto_library`. +* Суффикс `grpc` обычно используется для правил `go_grpc_library`. + +Часто используется одно слово, описывающее пакет: + +```go +// Хорошо: +import ( + foopb "path/to/package/foo_service_go_proto" + foogrpc "path/to/package/foo_service_go_grpc" +) +``` + +Следуйте рекомендациям по стилю для [имен +пакетов](https://google.github.io/styleguide/go/decisions#package-names). +Предпочитайте целые слова. Короткие имена хороши, но избегайте неоднозначности. +В случае сомнений используйте имя пакета proto до _go с суффиксом pb: + +```go +// Хорошо: +import ( + pushqueueservicepb "path/to/package/push_queue_service_go_proto" +) +``` + +**Примечание:** Предыдущие рекомендации поощряли очень короткие имена, такие как +"xpb" или даже просто "pb". Новый код должен предпочитать более описательные +имена. Существующий код, использующий короткие имена, не должен использоваться в +качестве примера, но его не нужно менять. + +<a id="import-order"></a> + +### Порядок импорта (Import ordering) + +См. [Go Style Decisions: Группировка импортов](https://neonxp.ru/pages/gostyleguide/google/decisions/.md#import-grouping). + +<a id="error-handling"></a> + +## Обработка ошибок (Error handling) + +В Go [ошибки — это значения (errors are values)]; они создаются кодом и +потребляются кодом. Ошибки могут быть: + +* Преобразованы в диагностическую информацию для отображения человеку +* Использованы сопровождающим +* Интерпретированы конечным пользователем + +Сообщения об ошибках также появляются на самых разных поверхностях, включая +сообщения журнала (log messages), дампы ошибок и отрисованные пользовательские +интерфейсы. + +Код, который обрабатывает (производит или потребляет) ошибки, должен делать это +осознанно. Может возникнуть соблазн проигнорировать или слепо распространить +возвращаемое значение ошибки. Однако всегда стоит подумать, находится ли текущая +функция в стеке вызовов в наилучшей позиции для обработки ошибки. Это обширная +тема, и трудно дать категоричные рекомендации. Используйте свое суждение, но +учитывайте следующие соображения: + +* Создавая значение ошибки, решите, придавать ли ему какую-либо + [структуру](#error-structure). +* Обрабатывая ошибку, рассмотрите возможность [добавления + информации](#error-extra-info), которая есть у вас, но которой может не быть + у вызывающей и/или вызываемой стороны. +* См. также рекомендации по [логированию ошибок](#error-logging). + +Хотя обычно нецелесообразно игнорировать ошибку, разумным исключением из этого +является оркестрация связанных операций, где часто только первая ошибка полезна. +Пакет [`errgroup`] предоставляет удобную абстракцию для группы операций, которые +могут завершиться ошибкой или быть отменены как группа. + +[ошибки — это значения (errors are values)]: + https://go.dev/blog/errors-are-values +[`errgroup`]: https://pkg.go.dev/golang.org/x/sync/errgroup + +См. также: + +* [Effective Go об ошибках](https://go.dev/doc/effective_go#errors) +* [Пост в блоге Go об ошибках](https://go.dev/blog/go1.13-errors) +* [Пакет `errors`](https://pkg.go.dev/errors) +* [Пакет + `upspin.io/errors`](https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html) +* [GoTip #89: When to Use Canonical Status Codes as + Errors](https://google.github.io/styleguide/go/index.html#gotip) +* [GoTip #48: Error Sentinel + Values](https://google.github.io/styleguide/go/index.html#gotip) +* [GoTip #13: Designing Errors for + Checking](https://google.github.io/styleguide/go/index.html#gotip) + +<a id="error-structure"></a> + +### Структура ошибок (Error structure) + +Если вызывающим сторонам необходимо анализировать ошибку (например, различать +различные условия ошибки), придайте значению ошибки структуру, чтобы это можно +было сделать программно, а не заставлять вызывающую сторону выполнять +сопоставление строк. Этот совет применим как к production-коду, так и к тестам, +которые заботятся о разных условиях ошибок. + +Простейшие структурированные ошибки — это непараметризованные глобальные +значения. + +```go +type Animal string + +var ( + // ErrDuplicate возникает, если это животное уже было замечено. + ErrDuplicate = errors.New("duplicate") + + // ErrMarsupial возникает, потому что у нас аллергия на сумчатых за пределами Австралии. + // Извините. + ErrMarsupial = errors.New("marsupials are not supported") +) + +func process(animal Animal) error { + switch { + case seen[animal]: + return ErrDuplicate + case marsupial(animal): + return ErrMarsupial + } + seen[animal] = true + // ... + return nil +} +``` + +Вызывающая сторона может просто сравнить возвращенное значение ошибки функции с +одним из известных значений ошибок: + +```go +// Хорошо: +func handlePet(...) { + switch err := process(an); err { + case ErrDuplicate: + return fmt.Errorf("feed %q: %v", an, err) + case ErrMarsupial: + // Попробуем восстановиться с помощью друга. + alternate = an.BackupAnimal() + return handlePet(..., alternate, ...) + } +} +``` + +Выше используются сторожевые (sentinel) значения, где ошибка должна быть равна +(в смысле `==`) ожидаемому значению. Во многих случаях это вполне адекватно. +Если `process` возвращает обернутые ошибки (wrapped errors) (обсуждается ниже), +вы можете использовать [`errors.Is`]. + +```go +// Хорошо: +func handlePet(...) { + switch err := process(an); { + case errors.Is(err, ErrDuplicate): + return fmt.Errorf("feed %q: %v", an, err) + case errors.Is(err, ErrMarsupial): + // ... + } +} +``` + +Не пытайтесь различать ошибки на основе их строковой формы. (См. [GoTip #13: +Designing Errors for +Checking](https://google.github.io/styleguide/go/index.html#gotip) для получения +дополнительной информации.) + +```go +// Плохо: +func handlePet(...) { + err := process(an) + if regexp.MatchString(`duplicate`, err.Error()) {...} + if regexp.MatchString(`marsupial`, err.Error()) {...} +} +``` + +Если в ошибке есть дополнительная информация, которая нужна вызывающей стороне +программно, ее, в идеале, следует представить структурно. Например, тип +[`os.PathError`] документирован так, что помещает путь к операции, завершившейся +неудачей, в поле структуры, к которому вызывающая сторона может легко получить +доступ. + +Могут использоваться и другие структуры ошибок, например, структура проекта, +содержащая код ошибки и строку с деталями. [Пакет `status`][status] — +распространенная инкапсуляция; если вы выбираете этот подход (вы не обязаны это +делать), используйте [канонические коды (canonical codes)]. См. [GoTip #89: +When to Use Canonical Status Codes as +Errors](https://google.github.io/styleguide/go/index.html#gotip) чтобы понять, +является ли использование кодов статуса правильным выбором. + +[`os.PathError`]: https://pkg.go.dev/os#PathError +[`errors.Is`]: https://pkg.go.dev/errors#Is +[`errors.As`]: https://pkg.go.dev/errors#As +[`package cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[status]: https://pkg.go.dev/google.golang.org/grpc/status +[канонические коды (canonical codes)]: + https://pkg.go.dev/google.golang.org/grpc/codes + +<a id="error-extra-info"></a> + +### Добавление информации к ошибкам (Adding information to errors) + +Добавляя информацию к ошибкам, избегайте избыточной информации, которую уже +предоставляет лежащая в основе ошибка. Пакет `os`, например, уже включает +информацию о пути в своих ошибках. + +```go +// Хорошо: +if err := os.Open("settings.txt"); err != nil { + return fmt.Errorf("launch codes unavailable: %v", err) +} + +// Вывод: +// +// launch codes unavailable: open settings.txt: no such file or directory +``` + +Здесь "launch codes unavailable" добавляет конкретный смысл ошибке `os.Open`, +релевантный для контекста текущей функции, без дублирования информации о пути к +файлу. + +```go +// Плохо: +if err := os.Open("settings.txt"); err != nil { + return fmt.Errorf("could not open settings.txt: %v", err) +} + +// Вывод: +// +// could not open settings.txt: open settings.txt: no such file or directory +``` + +Не добавляйте аннотацию, если ее единственная цель — указать на сбой без +добавления новой информации. Наличие ошибки достаточно передает сбой вызывающей +стороне. + +```go +// Плохо: +return fmt.Errorf("failed: %v", err) // просто верните err вместо этого +``` + +[Выбор между `%v` и `%w` при оборачивании ошибок (wrapping +errors)](https://go.dev/blog/go1.13-errors#whether-to-wrap) с помощью +`fmt.Errorf` — это тонкое решение, которое значительно влияет на то, как ошибки +распространяются, обрабатываются, проверяются и документируются в вашем +приложении. Основной принцип — сделать значения ошибок полезными для их +наблюдателей, будь то люди или код. + +1. **`%v` для простой аннотации или новой ошибки** + + Глагол `%v` — это ваш универсальный инструмент для строкового форматирования + любого значения Go, включая ошибки. При использовании с `fmt.Errorf` он + встраивает строковое представление ошибки (то, что возвращает ее метод + `Error()`) в новое значение ошибки, отбрасывая любую структурированную + информацию из исходной ошибки. Примеры использования `%v`: + + * Добавление интересного, не избыточного контекста: как в примере выше. + + * Логирование или отображение ошибок: Когда основная цель — представить + удобочитаемое сообщение об ошибке в журналах или пользователю, и вы не + планируете, чтобы вызывающая сторона программно проверяла ошибку с + помощью `errors.Is` или `errors.As` (Примечание: `errors.Unwrap` здесь, + как правило, не рекомендуется, так как он не обрабатывает множественные + ошибки (multi-errors)). + + * Создание новых, независимых ошибок: Иногда необходимо преобразовать + ошибку в новое сообщение об ошибке, тем самым скрывая специфику исходной + ошибки. Эта практика особенно полезна на границах систем, включая, + помимо прочего, RPC, IPC и хранилища, где мы переводим + доменно-специфичные ошибки в каноническое пространство ошибок. + + ```go + // Хорошо: + func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { + // ... + if err != nil { + return nil, fmt.Errorf("couldn't find fortune database: %v", err) + } + } + ``` + + Мы также могли бы явно аннотировать RPC код `Internal` в примере выше. + + ```go + // Хорошо: + import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + ) + + func (*FortuneTeller) SuggestFortune(context.Context, *pb.SuggestionRequest) (*pb.SuggestionResponse, error) { + // ... + if err != nil { + // Или используйте fmt.Errorf с глаголом %w, если намеренно оборачиваете ошибку, + // которую вызывающая сторона должна развернуть (unwrap). + return nil, status.Errorf(codes.Internal, "couldn't find fortune database", status.ErrInternal) + } + } + ``` + +1. **`%w` (wrap) для программной проверки и цепочки ошибок (error chaining)** + + Глагол `%w` специально предназначен для оборачивания ошибок (error + wrapping). Он создает новую ошибку, которая предоставляет метод `Unwrap()`, + позволяя вызывающим сторонам программно проверять цепочку ошибок с помощью + `errors.Is` и `errors.As`. Примеры использования `%w`: + + * Добавление контекста с сохранением исходной ошибки для программной + проверки: Это основной случай использования во вспомогательных функциях + (helpers) вашего приложения. Вы хотите обогатить ошибку дополнительным + контекстом (например, какая операция выполнялась, когда она завершилась + неудачей), но при этом позволить вызывающей стороне проверить, является + ли лежащая в основе ошибка конкретной сторожевой ошибкой или типом. + + ```go + // Хорошо: + func (s *Server) internalFunction(ctx context.Context) error { + // ... + if err != nil { + return fmt.Errorf("couldn't find remote file: %w", err) + } + } + ``` + + Это позволяет функции более высокого уровня выполнить `errors.Is(err, + fs.ErrNotExist)`, даже если исходная ошибка была обернута. + + В точках, где ваша система взаимодействует с внешними системами, такими + как RPC, IPC или хранилище, часто лучше переводить доменно-специфичные + ошибки в стандартизированное пространство ошибок (например, коды статуса + gRPC), а не просто оборачивать исходную ошибку с помощью `%w`. Клиента + обычно не волнует точная внутренняя ошибка файловой системы; их волнует + канонический результат (например, `Internal`, `NotFound`, + `PermissionDenied`). + + * Когда вы явно документируете и тестируете лежащие в основе ошибки, + которые вы раскрываете: Если API вашего пакета гарантирует, что + определенные лежащие в основе ошибки могут быть развернуты и проверены + вызывающими сторонами (например, "эта функция может вернуть + `ErrInvalidConfig`, обернутый в более общую ошибку"), то `%w` уместен. + Это становится частью контракта вашего пакета. + +См. также: + +* [Соглашения по документации ошибок (Error Documentation + Conventions)](#documentation-conventions-errors) +* [Пост в блоге об оборачивании ошибок](https://blog.golang.org/go1.13-errors) + +<a id="error-percent-w"></a> + +### Размещение `%w` в ошибках (Placement of %w in errors) + +Предпочитайте размещать `%w` в конце строки ошибки *если* вы используете +[оборачивание ошибок (error wrapping)](https://go.dev/blog/go1.13-errors) с +глаголом форматирования `%w`. + +Ошибки могут быть обернуты с помощью глагола `%w` или путем помещения их в +[структурированную +ошибку](https://google.github.io/styleguide/go/index.html#gotip), которая +реализует `Unwrap() error` (например, +[`fs.PathError`](https://pkg.go.dev/io/fs#PathError)). + +Обернутые ошибки образуют цепочки ошибок (error chains): каждый новый слой +обертывания добавляет новую запись в начало цепочки ошибок. Цепочку ошибок можно +обойти с помощью метода `Unwrap() error`. Например: + +```go +err1 := fmt.Errorf("err1") +err2 := fmt.Errorf("err2: %w", err1) +err3 := fmt.Errorf("err3: %w", err2) +``` + +Это формирует цепочку ошибок следующего вида: + +```mermaid +flowchart LR + err3 == err3 wraps err2 ==> err2; + err2 == err2 wraps err1 ==> err1; +``` + +Независимо от того, где размещен глагол `%w`, возвращаемая ошибка всегда +представляет начало цепочки ошибок, а `%w` — это следующий дочерний элемент. +Аналогично, `Unwrap() error` всегда обходит цепочку ошибок от самой новой к +самой старой ошибке. + +Однако размещение глагола `%w` влияет на то, печатается ли цепочка ошибок от +самой новой к самой старой, от самой старой к самой новой или ни то, ни другое: + +```go +// Хорошо: +err1 := fmt.Errorf("err1") +err2 := fmt.Errorf("err2: %w", err1) +err3 := fmt.Errorf("err3: %w", err2) +fmt.Println(err3) // err3: err2: err1 +// err3 — это цепочка ошибок от самой новой к самой старой, которая печатается от самой новой к самой старой. +``` + +```go +// Плохо: +err1 := fmt.Errorf("err1") +err2 := fmt.Errorf("%w: err2", err1) +err3 := fmt.Errorf("%w: err3", err2) +fmt.Println(err3) // err1: err2: err3 +// err3 — это цепочка ошибок от самой новой к самой старой, которая печатается от самой старой к самой новой. +``` + +```go +// Плохо: +err1 := fmt.Errorf("err1") +err2 := fmt.Errorf("err2-1 %w err2-2", err1) +err3 := fmt.Errorf("err3-1 %w err3-2", err2) +fmt.Println(err3) // err3-1 err2-1 err1 err2-2 err3-2 +// err3 — это цепочка ошибок от самой новой к самой старой, которая печатается ни от самой новой к самой старой, +// ни от самой старой к самой новой. +``` + +Поэтому, чтобы текст ошибки отражал структуру цепочки ошибок, предпочитайте +размещать глагол `%w` в конце в форме `[...]: %w`. + +<a id="error-logging"></a> + +### Логирование ошибок (Logging errors) + +Иногда функциям необходимо сообщить внешней системе об ошибке, не передавая ее +своим вызывающим сторонам. Логирование — очевидный выбор здесь; но будьте +внимательны к тому, что и как вы логируете. + +* Как и [хорошие сообщения о неудачных тестах (good test failure messages)], + сообщения журнала должны четко выражать, что пошло не так, и помогать + сопровождающему, включая соответствующую информацию для диагностики + проблемы. + +* Избегайте дублирования. Если вы возвращаете ошибку, обычно лучше не + логировать ее самостоятельно, а позволить вызывающей стороне обработать ее. + Вызывающая сторона может выбрать логирование ошибки или, возможно, + ограничить частоту логирования с помощью [`rate.Sometimes`]. Другие варианты + включают попытку восстановления или даже [остановку программы]. В любом + случае, предоставление контроля вызывающей стороне помогает избежать спама в + журналах. + + Однако обратной стороной этого подхода является то, что любое логирование + записывается с использованием координат строк вызывающей стороны. + +* Будьте осторожны с [PII]. Многие приемники журналов (log sinks) не являются + подходящими местами назначения для конфиденциальной информации конечных + пользователей. + +* Используйте `log.Error` скупо. Логирование уровня ERROR вызывает сброс + (flush) и является более дорогостоящим, чем более низкие уровни логирования. + Это может серьезно повлиять на производительность вашего кода. Принимая + решение между уровнями error и warning, учитывайте лучшую практику: + сообщения на уровне error должны быть actionable (то есть требовать + действий), а не просто "более серьезными", чем warning. + +* Внутри Google у нас есть системы мониторинга, которые можно настроить для + более эффективного оповещения, чем просто запись в файл журнала в надежде, + что кто-то его заметит. Это похоже, но не идентично стандартной библиотеке + [пакету `expvar`]. + +[хорошие сообщения о неудачных тестах (good test failure messages)]: + https://google.github.io/styleguide/go/decisions#useful-test-failures +[остановку программы]: #checks-and-panics +[`rate.Sometimes`]: https://pkg.go.dev/golang.org/x/time/rate#Sometimes +[PII]: https://en.wikipedia.org/wiki/Personal_data +[пакет `expvar`]: https://pkg.go.dev/expvar + +<a id="vlog"></a> + +#### Пользовательские уровни детализации (Custom verbosity levels) + +Используйте детальное логирование ([`log.V`]) с пользой. Детальное логирование +может быть полезно для разработки и трассировки. Установление соглашения об +уровнях детализации может быть полезным. Например: + +* Записывайте небольшое количество дополнительной информации на `V(1)` +* Трассируйте больше информации на `V(2)` +* Выводите большие внутренние состояния на `V(3)` + +Чтобы минимизировать стоимость детального логирования, вы должны убедиться, что +случайно не вызываете дорогие функции, даже когда `log.V` выключен. `log.V` +предлагает два API. Более удобный из них несет риск этих случайных затрат. В +случае сомнений используйте немного более многословный стиль. + +```go +// Хорошо: +for _, sql := range queries { + log.V(1).Infof("Handling %v", sql) + if log.V(2) { + log.Infof("Handling %v", sql.Explain()) + } + sql.Run(...) +} +``` + +```go +// Плохо: +// sql.Explain вызывается даже когда это сообщение не печатается. +log.V(2).Infof("Handling %v", sql.Explain()) +``` + +[`log.V`]: https://pkg.go.dev/github.com/golang/glog#V + +<a id="program-init"></a> + +### Инициализация программы (Program initialization) + +Ошибки инициализации программы (например, неправильные флаги и конфигурация) +должны передаваться вверх в `main`, который должен вызвать `log.Exit` с ошибкой, +объясняющей, как исправить ошибку. В этих случаях `log.Fatal` обычно не следует +использовать, потому что трассировка стека, указывающая на проверку, вряд ли +будет так полезна, как сгенерированное человеком, actionable сообщение. + +<a id="checks-and-panics"></a> + +### Проверки программы и паники (Program checks and panics) + +Как указано в [решении против паник (decision against panics)], стандартная +обработка ошибок должна быть структурирована вокруг возвращаемых значений +ошибок. Библиотеки должны предпочитать возвращать ошибку вызывающей стороне, а +не завершать программу, особенно для временных ошибок. + +Иногда необходимо выполнить проверку согласованности (consistency check) +инварианта и завершить программу, если он нарушен. Как правило, это делается +только в том случае, если сбой проверки инварианта означает, что внутреннее +состояние стало невосстановимым. Наиболее надежный способ сделать это в кодовой +базе Google — вызвать `log.Fatal`. Использование `panic` в этих случаях +ненадежно, потому что возможно, что отложенные (deferred) функции заблокируют +или еще больше повредят внутреннее или внешнее состояние. + +Аналогично, сопротивляйтесь искушению восстановить паники (recover panics), +чтобы избежать сбоев, так как это может привести к распространению поврежденного +состояния. Чем дальше вы от паники, тем меньше вы знаете о состоянии программы, +которая может удерживать блокировки или другие ресурсы. Затем программа может +развить другие неожиданные режимы сбоев, которые могут еще больше затруднить +диагностику проблемы. Вместо того чтобы пытаться обрабатывать неожиданные паники +в коде, используйте инструменты мониторинга для выявления неожиданных сбоев и +исправляйте связанные ошибки с высоким приоритетом. + +**Примечание:** Стандартный [`net/http` server] нарушает этот совет и +восстанавливает паники из обработчиков запросов. Консенсус среди опытных +инженеров Go заключается в том, что это была историческая ошибка. Если вы +исследуете журналы серверов приложений на других языках, часто можно найти +большие трассировки стека, которые остаются необработанными. Избегайте этой +ловушки в своих серверах. + +[решении против паник (decision against panics)]: + https://google.github.io/styleguide/go/decisions#dont-panic +[`net/http` server]: https://pkg.go.dev/net/http#Server + +<a id="when-to-panic"></a> + +### Когда использовать panic (When to panic) + +Стандартная библиотека вызывает panic при неправильном использовании API. +Например, [`reflect`] вызывает panic во многих случаях, когда значение доступно +таким образом, что предполагает его неправильную интерпретацию. Это аналогично +паникам на ошибки ядра языка, такие как доступ к элементу среза вне его границ. +Проверка кода и тесты должны обнаруживать такие ошибки, которые не ожидаются в +production-коде. Эти паники действуют как проверки инвариантов, которые не +зависят от библиотеки, поскольку стандартная библиотека не имеет доступа к +[уровневому пакету `log`], который используется в кодовой базе Google. + +[`reflect`]: https://pkg.go.dev/reflect +[уровневому пакету `log`]: decisions#logging + +Другой случай, когда паники могут быть полезны, хотя и нечасто, — это внутренняя +деталь реализации пакета, которая всегда имеет соответствующее восстановление +(recover) в цепочке вызовов. Парсеры и подобные глубоко вложенные, тесно +связанные внутренние группы функций могут выиграть от такого дизайна, где +проталкивание возвратов ошибок добавляет сложность без ценности. + +Ключевой атрибут этого дизайна заключается в том, что эти **паники никогда не +должны выходить за границы пакета** и не должны быть частью API пакета. Обычно +это достигается с помощью функции верхнего уровня с отложенным вызовом +(deferred), которая использует `recover` для преобразования распространенной +паники в возвращаемую ошибку на публичной границе API. Это требует, чтобы код, +который вызывает panic и восстанавливается, отличал паники, которые код вызывает +сам, от тех, которые он не вызывает: + +```go +// Хорошо: +type syntaxError struct { + msg string +} + +func parseInt(in string) int { + n, err := strconv.Atoi(in) + if err != nil { + panic(&syntaxError{"not a valid integer"}) + } +} + +func Parse(in string) (_ *Node, err error) { + defer func() { + if p := recover(); p != nil { + sErr, ok := p.(*syntaxError) + if !ok { + panic(p) // Распространяем panic, поскольку он находится вне области нашего кода. + } + err = fmt.Errorf("syntax error: %v", sErr.msg) + } + }() + ... // Парсим входные данные, вызывая parseInt внутри для парсинга целых чисел +} +``` + +> **Предупреждение:** Код, использующий этот шаблон, должен позаботиться об +> управлении любыми ресурсами, связанными с кодом, запущенным в таких разделах, +> управляемых defer (например, закрыть, освободить или разблокировать). +> +> См.: [Go Tip #81: Avoiding Resource Leaks in API Design] + +Паника также используется, когда компилятор не может идентифицировать +недостижимый код, например, при использовании функции типа `log.Fatal`, которая +не вернется: + +```go +// Хорошо: +func answer(i int) string { + switch i { + case 42: + return "yup" + case 54: + return "base 13, huh" + default: + log.Fatalf("Sorry, %d is not the answer.", i) + panic("unreachable") + } +} +``` + +[Не вызывайте функции `log` до того, как флаги будут +распарсены.](https://pkg.go.dev/github.com/golang/glog#pkg-overview) Если вы +должны завершиться в функции инициализации пакета (в `init` или +["must"-функции](https://neonxp.ru/pages/gostyleguide/google/decisions/#must-functions)), panic допустима вместо вызова +фатального логирования. + +См. также: + +* [Handling panics](https://go.dev/ref/spec#Handling_panics) и [Run-time + Panics](https://go.dev/ref/spec#Run_time_panics) в спецификации языка +* [Defer, Panic, and Recover](https://go.dev/blog/defer-panic-and-recover) +* [On the uses and misuses of panics in + Go](https://eli.thegreenplace.net/2018/on-the-uses-and-misuses-of-panics-in-go/) + +[Go Tip #81: Avoiding Resource Leaks in API Design]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="documentation"></a> + +## Документация (Documentation) + +<a id="documentation-conventions"></a> + +### Соглашения (Conventions) + +Этот раздел дополняет раздел [комментариев (commentary)] в документе решений. + +Код на Go, который документирован в знакомом стиле, легче читать и менее +вероятно, что его будут использовать неправильно, по сравнению с тем, что плохо +задокументировано или не задокументировано вовсе. Запускаемые [примеры +(examples)] появляются в Godoc и Code Search и являются отличным способом +объяснить, как использовать ваш код. + +[комментариев (commentary)]: decisions#commentary +[примеры (examples)]: decisions#examples + +<a id="documentation-conventions-params"></a> + +#### Параметры и конфигурация (Parameters and configuration) + +Не каждый параметр должен быть перечислен в документации. Это относится к: + +* параметрам функций и методов +* полям структур (struct fields) +* API для опций (options) + +Документируйте подверженные ошибкам или неочевидные поля и параметры, объясняя, +почему они интересны. + +В следующем фрагменте выделенный комментарий добавляет мало полезной информации +для читателя: + +```go +// Плохо: +// Sprintf форматирует в соответствии со спецификатором формата и возвращает результирующую строку. +// +// format — это формат, а data — данные для интерполяции. +func Sprintf(format string, data ...any) string +``` + +Однако этот фрагмент демонстрирует сценарий кода, похожий на предыдущий, где +комментарий вместо этого говорит что-то неочевидное или существенно полезное для +читателя: + +```go +// Хорошо: +// Sprintf форматирует в соответствии со спецификатором формата и возвращает результирующую строку. +// +// Предоставленные данные используются для интерполяции строки формата. Если данные не соответствуют +// ожидаемым глаголам формата или количество данных не удовлетворяет спецификации формата, +// функция будет встраивать предупреждения об ошибках форматирования в выходную строку, как описано +// в разделе "Format errors" выше. +func Sprintf(format string, data ...any) string +``` + +Учитывайте вашу вероятную аудиторию при выборе того, что документировать и на +какую глубину. Сопровождающие, новички в команде, внешние пользователи и даже вы +сами через шесть месяцев могут оценить немного другую информацию, отличную от +той, что у вас на уме, когда вы впервые начинаете писать документацию. + +См. также: + +* [GoTip #41: Identify Function Call Parameters] +* [GoTip #51: Patterns for Configuration] + +[GoTip #41: Identify Function Call Parameters]: + https://google.github.io/styleguide/go/index.html#gotip +[GoTip #51: Patterns for Configuration]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="documentation-conventions-contexts"></a> + +#### Контексты (Contexts) + +Подразумевается, что отмена (cancellation) аргумента контекста прерывает +функцию, которой он предоставлен. Если функция может возвращать ошибку, по +соглашению это `ctx.Err()`. + +Этот факт не нужно повторять: + +```go +// Плохо: +// Run выполняет рабочий цикл (run loop) воркера. +// +// Метод будет обрабатывать работу до отмены контекста и соответственно возвращает ошибку. +func (Worker) Run(ctx context.Context) error +``` + +Поскольку это подразумевается, следующее лучше: + +```go +// Хорошо: +// Run выполняет рабочий цикл воркера. +func (Worker) Run(ctx context.Context) error +``` + +Когда поведение контекста отличается или неочевидно, его следует прямо +задокументировать, если верно любое из следующего. + +* Функция возвращает ошибку, отличную от `ctx.Err()`, когда контекст отменен: + + ```go + // Хорошо: + // Run выполняет рабочий цикл воркера. + // + // Если контекст отменен, Run возвращает nil ошибку. + func (Worker) Run(ctx context.Context) error + ``` + +* Функция имеет другие механизмы, которые могут ее прервать или повлиять на + время жизни: + + ```go + // Хорошо: + // Run выполняет рабочий цикл воркера. + // + // Run обрабатывает работу до отмены контекста или вызова Stop. + // Отмена контекста обрабатывается асинхронно внутри: run может вернуться до того, + // как вся работа остановится. Метод Stop является синхронным и ожидает завершения + // всех операций из рабочего цикла. Используйте Stop для плавного завершения работы. + func (Worker) Run(ctx context.Context) error + + func (Worker) Stop() + ``` + +* Функция имеет особые ожидания относительно времени жизни контекста, его + происхождения (lineage) или прикрепленных значений (attached values): + + ```go + // Хорошо: + // NewReceiver начинает получать сообщения, отправленные в указанную очередь. + // Контекст не должен иметь дедлайна (deadline). + func NewReceiver(ctx context.Context) *Receiver + + // Principal возвращает человеко-читаемое имя стороны, совершившей вызов. + // Контекст должен иметь прикрепленное к нему значение из security.NewContext. + func Principal(ctx context.Context) (name string, ok bool) + ``` + + **Предупреждение:** Избегайте разработки API, которые предъявляют такие + требования (например, отсутствие дедлайнов у контекстов) от своих вызывающих + сторон. Вышеприведенное — лишь пример того, как это задокументировать, если + этого нельзя избежать, а не одобрение такого шаблона. + +<a id="documentation-conventions-concurrency"></a> + +#### Параллелизм (Concurrency) + +Пользователи Go предполагают, что концептуально доступные только для чтения +операции безопасны для параллельного использования и не требуют дополнительной +синхронизации. + +Дополнительное замечание о параллелизме можно безопасно удалить в этой Godoc: + +```go +// Хорошо: +// Len возвращает количество байт непрочитанной части буфера; +// b.Len() == len(b.Bytes()). +// +// Безопасно для вызова несколькими горутинами одновременно. +func (*Buffer) Len() int +``` + +Однако мутирующие операции не считаются безопасными для параллельного +использования и требуют, чтобы пользователь учитывал синхронизацию. + +Аналогично, дополнительное замечание о параллелизме можно безопасно удалить +здесь: + +```go +// Хорошо: +// Grow увеличивает емкость буфера. +// +// Не безопасно для вызова несколькими горутинами одновременно. +func (*Buffer) Grow(n int) +``` + +Настоятельно рекомендуется документировать, если верно любое из следующего. + +* Непонятно, является ли операция доступной только для чтения или мутирующей: + + ```go + // Хорошо: + package lrucache + + // Lookup возвращает данные, связанные с ключом, из кэша. + // + // Эта операция не безопасна для параллельного использования. + func (*Cache) Lookup(key string) (data []byte, ok bool) + ``` + + Почему? При попадании в кэш (cache hit) при поиске ключа внутреннее + состояние LRU-кэша мутирует. Как это реализовано, может быть неочевидно для + всех читателей. + +* Синхронизация предоставляется API: + + ```go + // Хорошо: + package fortune_go_proto + + // NewFortuneTellerClient возвращает *rpc.Client для сервиса FortuneTeller. + // Безопасно для одновременного использования несколькими горутинами. + func NewFortuneTellerClient(cc *rpc.ClientConn) *FortuneTellerClient + ``` + + Почему? Stubby предоставляет синхронизацию. + + **Примечание:** Если API является типом и API предоставляет синхронизацию в + целом, по соглашению только определение типа документирует семантику. + +* API потребляет пользовательские реализации типов или интерфейсов, и + потребитель интерфейса имеет особые требования к параллелизму: + + ```go + // Хорошо: + package health + + // Watcher сообщает о состоянии здоровья некоторой сущности (обычно серверной службы). + // + // Методы Watcher безопасны для одновременного использования несколькими горутинами. + type Watcher interface { + // Watch отправляет true на переданный канал, когда статус Watcher изменился. + Watch(changed chan<- bool) (unwatch func()) + + // Health возвращает nil, если за которой следят сущность здорова, или + // ненулевую ошибку, объясняющую, почему сущность нездорова. + Health() error + } + ``` + + Почему? Является ли API безопасным для использования несколькими горутинами + — это часть его контракта. + +<a id="documentation-conventions-cleanup"></a> + +#### Очистка (Cleanup) + +Документируйте любые явные требования к очистке, которые есть у API. В противном +случае вызывающие стороны будут использовать API неправильно, что приведет к +утечкам ресурсов и другим возможным ошибкам. + +Указывайте очистки, которые зависят от вызывающей стороны: + +```go +// Хорошо: +// NewTicker возвращает новый Ticker, содержащий канал, который будет отправлять +// текущее время на канал после каждого тика. +// +// Вызовите Stop, чтобы освободить ресурсы, связанные с Ticker, когда закончите. +func NewTicker(d Duration) *Ticker + +func (*Ticker) Stop() +``` + +Если может быть неясно, как очистить ресурсы, объясните, как: + +```go +// Хорошо: +// Get выполняет GET к указанному URL. +// +// Когда err равен nil, resp всегда содержит ненулевой resp.Body. +// Вызывающая сторона должна закрыть resp.Body, когда закончит читать из него. +// +// resp, err := http.Get("http://example.com/") +// if err != nil { +// // обработать ошибку +// } +// defer resp.Body.Close() +// body, err := io.ReadAll(resp.Body) +func (c *Client) Get(url string) (resp *Response, err error) +``` + +См. также: + +* [GoTip #110: Don’t Mix Exit With Defer] + +[GoTip #110: Don’t Mix Exit With Defer]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="documentation-conventions-errors"></a> + +#### Ошибки (Errors) + +Документируйте значимые сторожевые значения ошибок (error sentinel values) или +типы ошибок, которые ваши функции возвращают вызывающим сторонам, чтобы +вызывающие стороны могли предвидеть, какие типы условий они могут обработать в +своем коде. + +```go +// Хорошо: +package os + +// Read читает до len(b) байт из File и сохраняет их в b. Он возвращает +// количество прочитанных байт и любую встреченную ошибку. +// +// При достижении конца файла Read возвращает 0, io.EOF. +func (*File) Read(b []byte) (n int, err error) { +``` + +Когда функция возвращает определенный тип ошибки, правильно укажите, является ли +ошибка указателем (pointer receiver) или нет: + +```go +// Хорошо: +package os + +type PathError struct { + Op string + Path string + Err error +} + +// Chdir меняет текущий рабочий каталог на указанный каталог. +// +// Если есть ошибка, она будет типа *PathError. +func Chdir(dir string) error { +``` + +Документирование того, являются ли возвращаемые значения указателями, позволяет +вызывающим сторонам правильно сравнивать ошибки с помощью [`errors.Is`], +[`errors.As`] и [`package cmp`]. Это связано с тем, что не указатель +(non-pointer value) не эквивалентен указателю (pointer value). + +**Примечание:** В примере `Chdir` тип возвращаемого значения записан как +`error`, а не `*PathError`, из-за [как работают нулевые значения интерфейса (nil +interface values)](https://go.dev/doc/faq#nil_error). + +Документируйте общие соглашения об ошибках в [документации +пакета](https://neonxp.ru/pages/gostyleguide/google/decisions/#package-comments), когда поведение применимо к большинству +ошибок, встречающихся в пакете: + +```go +// Хорошо: +// Пакет os предоставляет независимый от платформы интерфейс к функциям операционной системы. +// +// Часто доступно больше информации внутри ошибки. Например, если вызов, принимающий имя файла, +// завершается неудачей, такой как Open или Stat, ошибка будет включать имя файла, которое +// не удалось, когда она печатается, и будет иметь тип *PathError, который может быть распакован +// для получения дополнительной информации. +package os +``` + +Вдумчивое применение этих подходов может добавить [дополнительную информацию к +ошибкам](#error-extra-info) без особых усилий и помочь вызывающим сторонам +избежать добавления избыточных аннотаций. + +См. также: + +* [Go Tip #106: Error Naming + Conventions](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #89: When to Use Canonical Status Codes as + Errors](https://google.github.io/styleguide/go/index.html#gotip) + +<a id="documentation-preview"></a> + +### Предварительный просмотр (Preview) + +Go имеет [сервер +документации](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite). +Рекомендуется предварительно просматривать документацию, которую производит ваш +код, как до, так и во время процесса ревью кода. Это помогает проверить, что +[форматирование godoc] отображается правильно. + +[форматирование godoc]: #godoc-formatting + +<a id="godoc-formatting"></a> + +### Форматирование Godoc (Godoc formatting) + +[Godoc] предоставляет специальный синтаксис для [форматирования документации]. + +* Требуется пустая строка для разделения абзацев: + + ```go + // Хорошо: + // LoadConfig читает конфигурацию из указанного файла. + // + // См. some/shortlink для подробностей о формате файла конфигурации. + ``` + +* Файлы тестов могут содержать [запускаемые примеры (runnable examples)], + которые появляются прикрепленными к соответствующей документации в godoc: + + ```go + // Хорошо: + func ExampleConfig_WriteTo() { + cfg := &Config{ + Name: "example", + } + if err := cfg.WriteTo(os.Stdout); err != nil { + log.Exitf("Failed to write config: %s", err) + } + // Output: + // { + // "name": "example" + // } + } + ``` + +* Отступ строк на два дополнительных пробела форматирует их буквально + (verbatim): + + ```go + // Хорошо: + // Update выполняет функцию в атомарной транзакции. + // + // Обычно это используется с анонимной TransactionFunc: + // + // if err := db.Update(func(state *State) { state.Foo = bar }); err != nil { + // //... + // } + ``` + + Однако обратите внимание, что часто может быть более уместно поместить код в + запускаемый пример, а не включать его в комментарий. + + Это буквальное форматирование может быть использовано для форматирования, не + родного для godoc, такого как списки и таблицы: + + ```go + // Хорошо: + // LoadConfig читает конфигурацию из указанного файла. + // + // LoadConfig обрабатывает следующие ключи особым образом: + // "import" заставит эту конфигурацию наследовать из указанного файла. + // "env" если присутствует, будет заполнен системным окружением. + ``` + +* Одна строка, которая начинается с заглавной буквы, не содержит знаков + препинания, кроме скобок и запятых, и за которой следует другой абзац, + форматируется как заголовок: + + ```go + // Хорошо: + // Следующая строка форматируется как заголовок. + // + // Использование заголовков + // + // Заголовки поставляются с автоматически сгенерированными якорными тегами для удобного связывания. + ``` + +[Godoc]: https://pkg.go.dev/ +[форматирования документации]: https://go.dev/doc/comment +[запускаемые примеры (runnable examples)]: decisions#examples + +<a id="signal-boost"></a> + +### Усиление сигнала (Signal boosting) + +Иногда строка кода выглядит как нечто обычное, но на самом деле это не так. Один +из лучших примеров этого — проверка `err == nil` (поскольку `err != nil` +встречается гораздо чаще). Следующие две условные проверки трудно различить: + +```go +// Хорошо: +if err := doSomething(); err != nil { + // ... +} +``` + +```go +// Плохо: +if err := doSomething(); err == nil { + // ... +} +``` + +Вы можете вместо этого "усилить" сигнал условного оператора, добавив +комментарий: + +```go +// Хорошо: +if err := doSomething(); err == nil { // если ошибки НЕТ + // ... +} +``` + +Комментарий привлекает внимание к различию в условном операторе. + +<a id="vardecls"></a> + +## Объявление переменных (Variable declarations) + +<a id="vardeclinitialization"></a> + +### Инициализация (Initialization) + +Для единообразия предпочитайте `:=` вместо `var` при инициализации новой +переменной ненулевым значением. + +```go +// Хорошо: +i := 42 +``` + +```go +// Плохо: +var i = 42 +``` + +<a id="vardeclzero"></a> + +### Объявление переменных с нулевыми значениями (Declaring variables with zero values) + +Следующие объявления используют [нулевое значение (zero value)]: + +```go +// Хорошо: +var ( + coords Point + magic [4]byte + primes []int +) +``` + +[нулевое значение (zero value)]: https://golang.org/ref/spec#The_zero_value + +Вы должны объявлять значения, используя нулевое значение, когда хотите передать +пустое значение, которое **готово к использованию позже**. Использование +составных литералов (composite literals) с явной инициализацией может быть +громоздким: + +```go +// Плохо: +var ( + coords = Point{X: 0, Y: 0} + magic = [4]byte{0, 0, 0, 0} + primes = []int(nil) +) +``` + +Распространенное применение объявления с нулевым значением — когда переменная +используется как выход при демаршалинге (unmarshalling): + +```go +// Хорошо: +var coords Point +if err := json.Unmarshal(data, &coords); err != nil { +``` + +Также допустимо использовать нулевое значение в следующей форме, когда вам нужна +переменная типа указателя: + +```go +// Хорошо: +msg := new(pb.Bar) // или "&pb.Bar{}" +if err := proto.Unmarshal(data, msg); err != nil { +``` + +Если в вашей структуре нужна блокировка (lock) или другое поле, которое [не +должно копироваться](https://neonxp.ru/pages/gostyleguide/google/decisions/#copying), вы можете сделать его типом значения +(value type), чтобы воспользоваться преимуществами инициализации нулевым +значением. Это означает, что содержащий тип теперь должен передаваться по +указателю, а не по значению. Методы этого типа должны принимать +получатели-указатели (pointer receivers). + +```go +// Хорошо: +type Counter struct { + // Это поле не обязательно должно быть "*sync.Mutex". Однако + // пользователи теперь должны передавать *Counter объекты между собой, а не Counter. + mu sync.Mutex + data map[string]int64 +} + +// Обратите внимание, что это должен быть получатель-указатель, чтобы предотвратить копирование. +func (c *Counter) IncrementBy(name string, n int64) +``` + +Допустимо использовать типы значений для локальных переменных составных типов +(таких как структуры и массивы), даже если они содержат такие некопируемые поля. +Однако, если составной тип возвращается функцией, или если все обращения к нему +в конечном итоге требуют взятия адреса, предпочтительнее объявить переменную как +тип указателя с самого начала. Аналогично, сообщения protobuf должны объявляться +как типы указателей. + +```go +// Хорошо: +func NewCounter(name string) *Counter { + c := new(Counter) // "&Counter{}" тоже подходит. + registerCounter(name, c) + return c +} + +var msg = new(pb.Bar) // или "&pb.Bar{}". +``` + +Это потому, что `*pb.Something` удовлетворяет [`proto.Message`], а +`pb.Something` — нет. + +```go +// Плохо: +func NewCounter(name string) *Counter { + var c Counter + registerCounter(name, &c) + return &c +} + +var msg = pb.Bar{} +``` + +[`proto.Message`]: https://pkg.go.dev/google.golang.org/protobuf/proto#Message + +> **Важно:** Типы map должны быть явно инициализированы перед тем, как их можно +> будет изменять. Однако чтение из map с нулевым значением вполне допустимо. +> +> Для типов map и slice, если код особенно чувствителен к производительности и +> если вы заранее знаете размеры, см. раздел [подсказки по размеру (size +> hints)](#vardeclsize). + +<a id="vardeclcomposite"></a> + +### Составные литералы (Composite literals) + +Следующие объявления являются [составными литералами (composite literal)]: + +```go +// Хорошо: +var ( + coords = Point{X: x, Y: y} + magic = [4]byte{'I', 'W', 'A', 'D'} + primes = []int{2, 3, 5, 7, 11} + captains = map[string]string{"Kirk": "James Tiberius", "Picard": "Jean-Luc"} +) +``` + +Вы должны объявлять значение с помощью составного литерала, когда знаете +начальные элементы или члены. + +В отличие от этого, использование составных литералов для объявления пустых +значений или значений без членов может быть визуально шумным по сравнению с +[инициализацией нулевым значением](#vardeclzero). + +Когда вам нужен указатель на нулевое значение, у вас есть два варианта: пустые +составные литералы и `new`. Оба варианта допустимы, но ключевое слово `new` +может служить напоминанием читателю, что если бы потребовалось ненулевое +значение, составной литерал не сработал бы: + +```go +// Хорошо: +var ( + buf = new(bytes.Buffer) // непустые Buffers инициализируются конструкторами. + msg = new(pb.Message) // непустые proto сообщения инициализируются билдерами или установкой полей по одному. +) +``` + +[составные литералы (composite literal)]: + https://golang.org/ref/spec#Composite_literals + +<a id="vardeclsize"></a> + +### Подсказки по размеру (Size hints) + +Следующие объявления используют подсказки по размеру, чтобы предварительно +выделить емкость: + +```go +// Хорошо: +var ( + // Предпочтительный размер буфера для целевой файловой системы: st_blksize. + buf = make([]byte, 131072) + // Обычно обрабатывается до 8-10 элементов за запуск (16 — безопасное предположение). + q = make([]Node, 0, 16) + // Каждый шард обрабатывает shardSize (обычно 32000+) элементов. + seen = make(map[string]bool, shardSize) +) +``` + +Подсказки по размеру и предварительное выделение — важные шаги **в сочетании с +эмпирическим анализом кода и его интеграций**, для создания производительного и +ресурсоэффективного кода. + +Большинству кода не нужны подсказки по размеру или предварительное выделение, и +он может позволить среде выполнения увеличивать срез или карту по мере +необходимости. Допустимо предварительно выделять память, когда окончательный +размер известен (например, при преобразовании между map и срезом), но это не +является требованием читаемости и может не стоить загромождения в простых +случаях. + +**Предупреждение:** Предварительное выделение больше памяти, чем нужно, может +тратить память в парке (fleet) или даже вредить производительности. В случае +сомнений см. [GoTip #3: Benchmarking Go Code] и по умолчанию используйте +[инициализацию нулевым значением](#vardeclzero) или [объявление составным +литералом](#vardeclcomposite). + +[GoTip #3: Benchmarking Go Code]: + https://google.github.io/styleguide/go/index.html#gotip + +<a id="decl-chan"></a> + +### Направление каналов (Channel direction) + +Указывайте [направление канала (channel direction)] там, где это возможно. + +```go +// Хорошо: +// sum вычисляет сумму всех значений. Она читает из канала до тех пор, +// пока канал не закроется. +func sum(values <-chan int) int { + // ... +} +``` + +Это предотвращает случайные ошибки программирования, которые возможны без +спецификации: + +```go +// Плохо: +func sum(values chan int) (out int) { + for v := range values { + out += v + } + // values уже должен быть закрыт для достижения этого кода, что означает, + // что второе закрытие вызовет панику. + close(values) +} +``` + +Когда направление указано, компилятор перехватывает простые ошибки, подобные +этой. Это также помогает передать меру владения (ownership) типу. + +См. также доклад Брайана Миллса "Rethinking Classical Concurrency Patterns": +[слайды][rethinking-concurrency-slides] [видео][rethinking-concurrency-video]. + +[rethinking-concurrency-slides]: + https://drive.google.com/file/d/1nPdvhB0PutEJzdCq5ms6UI58dp50fcAN/view?usp=sharing +[rethinking-concurrency-video]: https://www.youtube.com/watch?v=5zXAHh5tJqQ +[направление канала (channel direction)]: https://go.dev/ref/spec#Channel_types + +<a id="funcargs"></a> + +## Списки аргументов функций (Function argument lists) + +Не позволяйте сигнатуре функции становиться слишком длинной. По мере добавления +большего количества параметров в функцию роль отдельных параметров становится +менее ясной, а соседние параметры одного типа становится легче спутать. Функции +с большим количеством аргументов менее запоминаемы и их труднее читать в месте +вызова. + +При проектировании API рассмотрите возможность разделения высоконастраиваемой +функции, сигнатура которой становится сложной, на несколько более простых. Они +могут использовать общую (неэкспортируемую) реализацию, если это необходимо. + +Если функции требуется много входных данных, рассмотрите возможность введения +[структуры опций (option struct)] для некоторых аргументов или использование +более продвинутой техники [вариативных опций (variadic options)]. Основным +критерием выбора стратегии должно быть то, как выглядит вызов функции во всех +ожидаемых случаях использования. + +Приведенные ниже рекомендации в первую очередь применяются к экспортированным +API, к которым предъявляются более высокие стандарты, чем к неэкспортированным. +Эти методы могут быть не нужны для вашего случая использования. Используйте свое +суждение и балансируйте между принципами [ясности (clarity)] и [наименьшей +механизации (least mechanism)]. + +См. также: [Go Tip #24: Use Case-Specific +Constructions](https://google.github.io/styleguide/go/index.html#gotip) + +[структуры опций (option struct)]: #option-structure +[вариативных опций (variadic options)]: #variadic-options +[ясности (clarity)]: guide#clarity +[наименьшей механизации (least mechanism)]: guide#least-mechanism + +<a id="option-structure"></a> + +### Структура опций (Option structure) + +Структура опций (option structure) — это тип struct, который собирает некоторые +или все аргументы функции или метода, а затем передается в качестве последнего +аргумента функции или методу. (Структура должна быть экспортирована только если +она используется в экспортированной функции.) + +Использование структуры опций имеет ряд преимуществ: + +* Литерал структуры включает как поля, так и значения для каждого аргумента, + что делает их самодокументированными и затрудняет их перестановку. +* Несущественные или "значения по умолчанию" поля могут быть опущены. +* Вызывающие стороны могут совместно использовать структуру опций и писать + вспомогательные функции для работы с ней. +* Структуры обеспечивают более чистую документацию для каждого поля, чем + аргументы функций. +* Структуры опций могут расти со временем без влияния на места вызова. + +Вот пример функции, которую можно улучшить: + +```go +// Плохо: +func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { + // ... +} +``` + +Функция выше может быть переписана со структурой опций следующим образом: + +```go +// Хорошо: +type ReplicationOptions struct { + Config *replicator.Config + PrimaryRegions []string + ReadonlyRegions []string + ReplicateExisting bool + OverwritePolicies bool + ReplicationInterval time.Duration + CopyWorkers int + HealthWatcher health.Watcher +} + +func EnableReplication(ctx context.Context, opts ReplicationOptions) { + // ... +} +``` + +Затем функцию можно вызвать в другом пакете: + +```go +// Хорошо: +func foo(ctx context.Context) { + // Сложный вызов: + storage.EnableReplication(ctx, storage.ReplicationOptions{ + Config: config, + PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, + ReadonlyRegions: []string{"us-east5", "us-central6"}, + OverwritePolicies: true, + ReplicationInterval: 1 * time.Hour, + CopyWorkers: 100, + HealthWatcher: watcher, + }) + + // Простой вызов: + storage.EnableReplication(ctx, storage.ReplicationOptions{ + Config: config, + PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, + }) +} +``` + +**Примечание:** [Контексты никогда не включаются в структуры +опций](https://neonxp.ru/pages/gostyleguide/google/decisions/#contexts). + +Этот вариант часто предпочтителен, когда применимо одно из следующих условий: + +* Все вызывающие стороны должны указать одну или несколько опций. +* Большому количеству вызывающих сторон необходимо предоставить множество + опций. +* Опции используются совместно несколькими функциями, которые будет вызывать + пользователь. + +<a id="variadic-options"></a> + +### Вариативные опции (Variadic options) + +Используя вариативные опции, создаются экспортированные функции, которые +возвращают замыкания (closures), которые могут быть переданы в [вариативный +(`...`) параметр] функции. Функция принимает в качестве параметров значения +опции (если есть), а возвращаемое замыкание принимает изменяемую ссылку (обычно +указатель на тип struct), которая будет обновлена на основе входных данных. + +[вариативный (`...`) параметр]: + https://golang.org/ref/spec#Passing_arguments_to_..._parameters + +Использование вариативных опций может предоставить ряд преимуществ: + +* Опции не занимают места в месте вызова, когда конфигурация не нужна. +* Опции все еще являются значениями, поэтому вызывающие стороны могут делиться + ими, писать вспомогательные функции и накапливать их. +* Опции могут принимать несколько параметров (например, + `cartesian.Translate(dx, dy int) TransformOption`). +* Функции опций могут возвращать именованный тип, чтобы группировать опции + вместе в godoc. +* Пакеты могут разрешать (или запрещать) сторонним пакетам определять (или + запрещать определение) свои собственные опции. + +**Примечание:** Использование вариативных опций требует значительного количества +дополнительного кода (см. следующий пример), поэтому их следует использовать +только тогда, когда преимущества перевешивают накладные расходы. + +Вот пример функции, которую можно улучшить: + +```go +// Плохо: +func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { + ... +} +``` + +Пример выше может быть переписан с вариативными опциями следующим образом: + +```go +// Хорошо: +type replicationOptions struct { + readonlyCells []string + replicateExisting bool + overwritePolicies bool + replicationInterval time.Duration + copyWorkers int + healthWatcher health.Watcher +} + +// ReplicationOption настраивает EnableReplication. +type ReplicationOption func(*replicationOptions) + +// ReadonlyCells добавляет дополнительные ячейки, которые дополнительно +// должны содержать реплики только для чтения данных. +// +// Передача этой опции несколько раз добавит дополнительные +// ячейки только для чтения. +// +// По умолчанию: нет +func ReadonlyCells(cells ...string) ReplicationOption { + return func(opts *replicationOptions) { + opts.readonlyCells = append(opts.readonlyCells, cells...) + } +} + +// ReplicateExisting контролирует, будут ли файлы, уже существующие в +// первичных ячейках, реплицированы. В противном случае только недавно добавленные +// файлы будут кандидатами на репликацию. +// +// Повторная передача этой опции перезапишет предыдущие значения. +// +// По умолчанию: false +func ReplicateExisting(enabled bool) ReplicationOption { + return func(opts *replicationOptions) { + opts.replicateExisting = enabled + } +} + +// ... другие опции ... + +// DefaultReplicationOptions управляют значениями по умолчанию перед +// применением опций, переданных в EnableReplication. +var DefaultReplicationOptions = []ReplicationOption{ + OverwritePolicies(true), + ReplicationInterval(12 * time.Hour), + CopyWorkers(10), +} + +func EnableReplication(ctx context.Context, config *placer.Config, primaryCells []string, opts ...ReplicationOption) { + var options replicationOptions + for _, opt := range DefaultReplicationOptions { + opt(&options) + } + for _, opt := range opts { + opt(&options) + } +} +``` + +Затем функцию можно вызвать в другом пакете: + +```go +// Хорошо: +func foo(ctx context.Context) { + // Сложный вызов: + storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}, + storage.ReadonlyCells("ix", "gg"), + storage.OverwritePolicies(true), + storage.ReplicationInterval(1*time.Hour), + storage.CopyWorkers(100), + storage.HealthWatcher(watcher), + ) + + // Простой вызов: + storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}) +} +``` + +Предпочитайте этот вариант, когда применимо большинство из следующего: + +* Большинству вызывающих сторон не нужно указывать никакие опции. +* Большинство опций используется редко. +* Существует большое количество опций. +* Опции требуют аргументов. +* Опции могут завершиться неудачей или быть установлены неправильно (в этом + случае функция опции возвращает `error`). +* Опции требуют большого количества документации, которую трудно уместить в + структуре. +* Пользователи или другие пакеты могут предоставлять пользовательские опции. + +Опции в этом стиле должны принимать параметры, а не использовать наличие +(presence) для сигнализации своего значения; последнее может значительно +усложнить динамическое составление аргументов. Например, двоичные настройки +должны принимать логическое значение (например, `rpc.FailFast(enable bool)` +предпочтительнее, чем `rpc.EnableFailFast()`). Перечисляемая опция должна +принимать перечисляемую константу (например, `log.Format(log.Capacitor)` +предпочтительнее, чем `log.CapacitorFormat()`). Альтернатива значительно +усложняет жизнь пользователям, которые должны программно выбирать, какие опции +передавать; такие пользователи вынуждены изменять фактический состав параметров, +а не просто изменять аргументы опций. Не предполагайте, что все пользователи +будут статически знать полный набор опций. + +Как правило, опции должны обрабатываться по порядку. Если возникает конфликт или +если некумулятивная опция передается несколько раз, должен побеждать последний +аргумент. + +Параметр функции опции в этом шаблоне обычно не экспортируется, чтобы ограничить +определение опций только самим пакетом. Это хороший вариант по умолчанию, хотя +могут быть случаи, когда уместно позволить другим пакетам определять опции. + +См. [оригинальный пост в блоге Роба Пайка] и [доклад Дейва Ченея] для более +глубокого изучения того, как эти опции могут быть использованы. + +[оригинальный пост в блоге Роба Пайка]: + http://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html +[доклад Дейва Ченея]: + https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis + +<a id="complex-clis"></a> + +## Сложные интерфейсы командной строки (Complex command-line interfaces) + +Некоторые программы хотят предоставить пользователям богатый интерфейс командной +строки, включающий подкоманды. Например, `kubectl create`, `kubectl run` и +многие другие подкоманды предоставляются программой `kubectl`. Существует по +крайней мере следующие общеупотребительные библиотеки для достижения этого. + +Если у вас нет предпочтений или другие соображения равны, рекомендуется +[subcommands], поскольку она самая простая и с ней легко работать правильно. +Однако, если вам нужны другие функции, которые она не предоставляет, выберите +один из других вариантов. + +* **[cobra]** + + * Соглашение о флагах: getopt + * Распространена за пределами кодовой базы Google. + * Много дополнительных функций. + * Подводные камни в использовании (см. ниже). + +* **[subcommands]** + + * Соглашение о флагах: Go + * Проста и с ней легко работать правильно. + * Рекомендуется, если вам не нужны дополнительные функции. + +**Предупреждение**: функции команд cobra должны использовать `cmd.Context()` для +получения контекста, а не создавать свой собственный корневой контекст с помощью +`context.Background`. Код, использующий пакет subcommands, уже получает +правильный контекст как параметр функции. + +Вы не обязаны помещать каждую подкоманду в отдельный пакет, и часто в этом нет +необходимости. Применяйте те же соображения о границах пакетов, что и в любой +кодовой базе Go. Если ваш код может использоваться как библиотека и как бинарный +файл, обычно полезно отделить CLI-код от библиотеки, делая CLI просто еще одним +из ее клиентов. (Это не специфично для CLI с подкомандами, но упоминается здесь, +потому что это частое место, где это возникает.) + +[subcommands]: https://pkg.go.dev/github.com/google/subcommands +[cobra]: https://pkg.go.dev/github.com/spf13/cobra + +<a id="tests"></a> + +## Тесты (Tests) + +<a id="test-functions"></a> + +### Оставляйте тестирование функции `Test` + +<!-- Примечание для сопровождающих: Этот раздел пересекается с decisions#assert и +decisions#mark-test-helpers. Цель не в том, чтобы повторять информацию, а +в том, чтобы иметь одно место, которое суммирует различие, о котором часто +задумываются новички в языке. --> + +Go различает "тестовые помощники (test helpers)" и "помощники утверждений +(assertion helpers)": + +* **Тестовые помощники** — это функции, которые выполняют задачи настройки или + очистки. Все сбои, которые происходят в тестовых помощниках, ожидаемо + являются сбоями окружения (а не тестируемого кода) — например, когда + тестовая база данных не может быть запущена, потому что на этой машине + больше нет свободных портов. Для таких функций часто уместно вызывать + `t.Helper`, чтобы [пометить их как тестовый помощник]. См. [обработку ошибок + в тестовых помощниках] для более подробной информации. + +* **Помощники утверждений** — это функции, которые проверяют правильность + системы и завершают тест с ошибкой, если ожидание не выполняется. Помощники + утверждений [не считаются идиоматичными] в Go. + +Цель теста — сообщить о условиях прохождения/непрохождения тестируемого кода. +Идеальное место для завершения теста с ошибкой — внутри самой функции `Test`, +так как это обеспечивает ясность [сообщений об ошибках] и логики теста. + +[пометить их как тестовый помощник]: decisions#mark-test-helpers +[обработку ошибок в тестовых помощниках]: #test-helper-error-handling +[не считаются идиоматичными]: decisions#assert +[сообщений об ошибках]: decisions#useful-test-failures + +По мере роста вашего тестового кода может стать необходимым вынести некоторую +функциональность в отдельные функции. Стандартные соображения программной +инженерии все еще применяются, поскольку *тестовый код — это все еще код*. Если +функциональность не взаимодействует с тестовым фреймворком, то применяются все +обычные правила. Однако, когда общий код взаимодействует с фреймворком, +необходимо соблюдать осторожность, чтобы избежать распространенных подводных +камней, которые могут привести к неинформативным сообщениям об ошибках и +неудобным в поддержке тестам. + +Если многим отдельным тестовым случаям требуется одна и та же логика валидации, +организуйте тест одним из следующих способов вместо использования помощников +утверждений или сложных функций валидации: + +* Встройте логику (и валидацию, и завершение с ошибкой) в функцию `Test`, даже + если это повторяется. Это лучше всего работает в простых случаях. +* Если входные данные похожи, рассмотрите возможность объединения их в + [табличный тест (table-driven test)], сохраняя логику встроенной в цикл. Это + помогает избежать повторения, сохраняя валидацию и завершение с ошибкой в + `Test`. +* Если есть несколько вызывающих сторон, которым нужна одна и та же функция + валидации, но табличные тесты не подходят (обычно потому, что входные данные + недостаточно просты или валидация требуется как часть последовательности + операций), организуйте функцию валидации так, чтобы она возвращала значение + (обычно `error`), а не принимала параметр `testing.T` и использовала его для + завершения теста с ошибкой. Используйте логику внутри `Test`, чтобы решить, + завершать ли тест с ошибкой, и предоставить [полезные сообщения об ошибках + теста]. Вы также можете создать тестовые помощники для выноса общего + шаблонного кода настройки. + +Дизайн, описанный в последнем пункте, сохраняет ортогональность. Например, +[пакет `cmp`] не предназначен для завершения тестов с ошибкой, а для сравнения +(и вычисления различий) значений. Поэтому ему не нужно знать о контексте, в +котором было сделано сравнение, поскольку вызывающая сторона может предоставить +его. Если ваш общий тестовый код предоставляет `cmp.Transformer` для вашего типа +данных, это часто может быть самым простым дизайном. Для других проверок +рассмотрите возможность возврата значения `error`. + +```go +// Хорошо: +// polygonCmp возвращает cmp.Option, которое приравнивает объекты геометрии s2 +// с некоторой небольшой ошибкой с плавающей точкой. +func polygonCmp() cmp.Option { + return cmp.Options{ + cmp.Transformer("polygon", func(p *s2.Polygon) []*s2.Loop { return p.Loops() }), + cmp.Transformer("loop", func(l *s2.Loop) []s2.Point { return l.Vertices() }), + cmpopts.EquateApprox(0.00000001, 0), + cmpopts.EquateEmpty(), + } +} + +func TestFenceposts(t *testing.T) { + // Это тест для вымышленной функции Fenceposts, которая рисует забор + // вокруг некоторого объекта Place. Детали не важны, за исключением того, + // что результат — это некоторый объект, имеющий геометрию s2 (github.com/golang/geo/s2) + got := Fencepost(tomsDiner, 1*meter) + if diff := cmp.Diff(want, got, polygonCmp()); diff != "" { + t.Errorf("Fencepost(tomsDiner, 1m) returned unexpected diff (-want+got):\n%v", diff) + } +} + +func FuzzFencepost(f *testing.F) { + // Фаззинг-тест (https://go.dev/doc/fuzz) для того же. + + f.Add(tomsDiner, 1*meter) + f.Add(school, 3*meter) + + f.Fuzz(func(t *testing.T, geo Place, padding Length) { + got := Fencepost(geo, padding) + // Простая эталонная реализация: не используется в prod, но проста для + // понимания и поэтому полезна для проверки в случайных тестах. + reference := slowFencepost(geo, padding) + + // Во фаззинг-тесте входные и выходные данные могут быть большими, поэтому + // не беспокойтесь о печати diff. cmp.Equal достаточно. + if !cmp.Equal(got, reference, polygonCmp()) { + t.Errorf("Fencepost returned wrong placement") + } + }) +} +``` + +Функция `polygonCmp` агностична относительно того, как ее вызывают; она не +принимает конкретный тип входных данных и не контролирует, что делать, если два +объекта не совпадают. Поэтому больше вызывающих сторон могут использовать ее. + +**Примечание:** Существует аналогия между тестовыми помощниками и обычным +библиотечным кодом. Код в библиотеках обычно [не должен вызывать panic] за +редкими исключениями; код, вызываемый из теста, не должен останавливать тест, +если нет [смысла продолжать]. + +[табличный тест (table-driven test)]: decisions#table-driven-tests +[полезные сообщения об ошибках теста]: decisions#useful-test-failures +[пакет `cmp`]: https://pkg.go.dev/github.com/google/go-cmp/cmp +[не должен вызывать panic]: decisions#dont-panic +[смысла продолжать]: #t-fatal + +<a id="test-validation-apis"></a> + +### Проектирование расширяемых API валидации (Designing extensible validation APIs) + +Большая часть советов о тестировании в руководстве по стилю касается +тестирования вашего собственного кода. Этот раздел о том, как предоставить +средства для других людей тестировать код, который они пишут, чтобы убедиться, +что он соответствует требованиям вашей библиотеки. + +<a id="test-validation-apis-what"></a> + +#### Приемочное тестирование (Acceptance testing) + +Такое тестирование называется [приемочным тестированием (acceptance testing)]. +Предпосылка такого тестирования заключается в том, что человек, использующий +тест, не знает всех деталей того, что происходит в тесте; он просто передает +входные данные в тестовое средство, чтобы оно выполнило работу. Это можно +рассматривать как форму [инверсии управления (inversion of control)]. + +В типичном тесте Go тестовая функция контролирует поток программы, и +рекомендации [без утверждений (no assert)](https://neonxp.ru/pages/gostyleguide/google/decisions/#assert) и [тестовые +функции](#test-functions) побуждают вас сохранять это так. Этот раздел +объясняет, как создавать поддержку для таких тестов способом, согласующимся со +стилем Go. + +Прежде чем углубляться в "как", рассмотрим пример из [`io/fs`], приведенный +ниже: + +```go +type FS interface { + Open(name string) (File, error) +} +``` + +Хотя существуют хорошо известные реализации `fs.FS`, от разработчика Go может +потребоваться создать свою. Чтобы помочь проверить правильность пользовательской +реализации `fs.FS`, была предоставлена универсальная библиотека в +[`testing/fstest`] под названием [`fstest.TestFS`]. Этот API рассматривает +реализацию как черный ящик (blackbox), чтобы убедиться, что она соблюдает самые +основные части контракта `io/fs`. + +[приемочным тестированием (acceptance testing)]: + https://en.wikipedia.org/wiki/Acceptance_testing +[инверсии управления (inversion of control)]: + https://en.wikipedia.org/wiki/Inversion_of_control +[`io/fs`]: https://pkg.go.dev/io/fs +[`testing/fstest`]: https://pkg.go.dev/testing/fstest +[`fstest.TestFS`]: https://pkg.go.dev/testing/fstest#TestFS + +<a id="test-validation-apis-writing"></a> + +#### Написание приемочного теста (Writing an acceptance test) + +Теперь, когда мы знаем, что такое приемочный тест и почему вы можете его +использовать, давайте рассмотрим создание приемочного теста для `package chess`, +пакета, используемого для симуляции шахматных игр. Пользователи `chess` должны +реализовать интерфейс `chess.Player`. Эти реализации — основное, что мы будем +проверять. Наш приемочный тест касается того, делает ли реализация игрока +легальные ходы, а не того, являются ли ходы умными. + +1. Создайте новый пакет для поведения валидации, [обычно + именуемый](#naming-doubles-helper-package) добавлением слова `test` к имени + пакета (например, `chesstest`). + +1. Создайте функцию, которая выполняет валидацию, принимая тестируемую + реализацию в качестве аргумента и проверяя ее: + + ```go + // ExercisePlayer тестирует реализацию Player за один ход на доске. + // Сама доска выборочно проверяется на разумность и правильность. + // + // Возвращает nil ошибку, если игрок делает правильный ход в контексте + // предоставленной доски. В противном случае ExercisePlayer возвращает одну из + // ошибок этого пакета, чтобы указать, как и почему игрок не прошел валидацию. + func ExercisePlayer(b *chess.Board, p chess.Player) error + ``` + + Тест должен отмечать, какие инварианты нарушены и как. Ваш дизайн может + выбрать одну из двух дисциплин для сообщения о сбоях: + + * **Завершение при первой ошибке (Fail fast)**: возвращать ошибку, как + только реализация нарушает инвариант. + + Это самый простой подход, и он хорошо работает, если ожидается, что + приемочный тест будет выполняться быстро. Простые [сторожевые ошибки + (sentinels)] и [пользовательские типы] могут быть легко использованы + здесь, что, в свою очередь, облегчает тестирование самого приемочного + теста. + + ```go + for color, army := range b.Armies { + // Король никогда не должен покидать доску, потому что игра заканчивается + // матом. + if army.King == nil { + return &MissingPieceError{Color: color, Piece: chess.King} + } + } + ``` + + * **Агрегация всех сбоев (Aggregate all failures)**: собирать все сбои и + сообщать о них всех. + + Этот подход напоминает рекомендацию [продолжать выполнение (keep going)] + и может быть предпочтительнее, если ожидается, что приемочный тест будет + выполняться медленно. + + То, как вы агрегируете сбои, должно определяться тем, хотите ли вы дать + пользователям или себе возможность исследовать отдельные сбои (например, + для тестирования вашего приемочного теста). Ниже демонстрируется + использование [пользовательского типа ошибки][пользовательские типы], + который [агрегирует ошибки]: + + ```go + var badMoves []error + + move := p.Move() + if putsOwnKingIntoCheck(b, move) { + badMoves = append(badMoves, PutsSelfIntoCheckError{Move: move}) + } + + if len(badMoves) > 0 { + return SimulationError{BadMoves: badMoves} + } + return nil + ``` + +Приемочный тест должен соблюдать рекомендацию [продолжать выполнение (keep +going)], не вызывая `t.Fatal`, если тест не обнаруживает нарушение инварианта в +системе, которая тестируется. + +Например, `t.Fatal` должен быть зарезервирован для исключительных случаев, таких +как [сбой настройки](#test-helper-error-handling), как обычно: + +```go +func ExerciseGame(t *testing.T, cfg *Config, p chess.Player) error { + t.Helper() + + if cfg.Simulation == Modem { + conn, err := modempool.Allocate() + if err != nil { + t.Fatalf("No modem for the opponent could be provisioned: %v", err) + } + t.Cleanup(func() { modempool.Return(conn) }) + } + // Запустить приемочный тест (целую игру). +} +``` + +Эта техника может помочь вам создавать лаконичные, каноничные проверки. Но не +пытайтесь использовать ее, чтобы обойти [рекомендации об +утверждениях](https://neonxp.ru/pages/gostyleguide/google/decisions/#assert). + +Конечный продукт должен быть похож на этот для конечных пользователей: + +```go +// Хорошо: +package deepblue_test + +import ( + "chesstest" + "deepblue" +) + +func TestAcceptance(t *testing.T) { + player := deepblue.New() + err := chesstest.ExerciseGame(t, chesstest.SimpleGame, player) + if err != nil { + t.Errorf("Deep Blue player failed acceptance test: %v", err) + } +} +``` + +[сторожевые ошибки (sentinels)]: + https://google.github.io/styleguide/go/index.html#gotip +[пользовательские типы]: https://google.github.io/styleguide/go/index.html#gotip +[агрегирует ошибки]: https://google.github.io/styleguide/go/index.html#gotip + +<a id="use-real-transports"></a> + +### Используйте реальные транспорты (Use real transports) + +При тестировании интеграции компонентов, особенно когда HTTP или RPC +используются в качестве базового транспорта между компонентами, предпочитайте +использовать реальный базовый транспорт для подключения к тестовой версии +бэкенда. + +Например, предположим, что код, который вы хотите протестировать (иногда +называемый "системой под тестом" или SUT), взаимодействует с бэкендом, +реализующим API [долго выполняющихся операций (long running operations)]. Чтобы +протестировать ваш SUT, используйте реальный [OperationsClient], подключенный к +[тестовому двойнику (test +double)](https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts) +(например, моку, заглушке или фейку) [OperationsServer]. + +[тестовому двойнику (test double)]: + https://abseil.io/resources/swe-book/html/ch13.html#basic_concepts +[долго выполняющихся операций (long running operations)]: + https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning +[OperationsClient]: + https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning#OperationsClient +[OperationsServer]: + https://pkg.go.dev/google.golang.org/genproto/googleapis/longrunning#OperationsServer + +Это рекомендуется вместо ручной реализации клиента из-за сложности правильной +имитации поведения клиента. Используя production-клиент с тестовым сервером, вы +гарантируете, что ваш тест использует как можно больше реального кода. + +**Совет:** По возможности используйте тестовую библиотеку, предоставленную +авторами тестируемого сервиса. + +<a id="t-fatal"></a> + +### `t.Error` против `t.Fatal` + +Как обсуждалось в [решениях](https://neonxp.ru/pages/gostyleguide/google/decisions/#keep-going), тесты, как правило, не +должны прерываться при первой встреченной проблеме. + +Однако некоторые ситуации требуют, чтобы тест не продолжался. Вызов `t.Fatal` +уместен, когда какая-то часть настройки теста завершается неудачей, особенно во +[вспомогательных функциях настройки теста], без которых вы не можете запустить +остальную часть теста. В табличном тесте `t.Fatal` уместен для сбоев, которые +настраивают всю тестовую функцию до начала цикла теста. Сбои, которые +затрагивают одну запись в таблице теста и делают невозможным продолжение работы +с этой записью, должны сообщаться следующим образом: + +* Если вы не используете подтесты `t.Run`, используйте `t.Error`, за которым + следует оператор `continue` для перехода к следующей записи таблицы. +* Если вы используете подтесты (и вы внутри вызова `t.Run`), используйте + `t.Fatal`, который завершает текущий подтест и позволяет вашему тестовому + случаю перейти к следующему подтесту. + +**Предупреждение:** Не всегда безопасно вызывать `t.Fatal` и подобные функции. +[Подробнее здесь](#t-fatal-goroutine). + +[вспомогательных функциях настройки теста]: #test-helper-error-handling + +<a id="test-helper-error-handling"></a> + +### Обработка ошибок во вспомогательных тестовых функциях (Error handling in test helpers) + +**Примечание:** В этом разделе обсуждаются [тестовые помощники (test helpers)] в +том смысле, в котором Go использует этот термин: функции, которые выполняют +настройку и очистку теста, а не общие средства утверждений. См. раздел [тестовые +функции](#test-functions) для более подробного обсуждения. + +[тестовые помощники (test helpers)]: decisions#mark-test-helpers + +Операции, выполняемые тестовым помощником, иногда завершаются неудачей. +Например, настройка каталога с файлами включает ввод-вывод, который может +завершиться неудачей. Когда тестовые помощники завершаются неудачей, их сбой +часто означает, что тест не может продолжиться, поскольку не выполнилось +предварительное условие настройки. Когда это происходит, предпочтительнее +вызвать одну из функций `Fatal` в помощнике: + +```go +// Хорошо: +func mustAddGameAssets(t *testing.T, dir string) { + t.Helper() + if err := os.WriteFile(path.Join(dir, "pak0.pak"), pak0, 0644); err != nil { + t.Fatalf("Setup failed: could not write pak0 asset: %v", err) + } + if err := os.WriteFile(path.Join(dir, "pak1.pak"), pak1, 0644); err != nil { + t.Fatalf("Setup failed: could not write pak1 asset: %v", err) + } +} +``` + +Это делает вызывающую сторону чище, чем если бы помощник возвращал ошибку самому +тесту: + +```go +// Плохо: +func addGameAssets(t *testing.T, dir string) error { + t.Helper() + if err := os.WriteFile(path.Join(d, "pak0.pak"), pak0, 0644); err != nil { + return err + } + if err := os.WriteFile(path.Join(d, "pak1.pak"), pak1, 0644); err != nil { + return err + } + return nil +} +``` + +**Предупреждение:** Не всегда безопасно вызывать `t.Fatal` и подобные функции. +[Подробнее](#t-fatal-goroutine) здесь. + +Сообщение об ошибке должно включать описание того, что произошло. Это важно, так +как вы можете предоставлять тестовый API многим пользователям, особенно с +увеличением количества шагов, производящих ошибки, в помощнике. Когда тест +завершается неудачей, пользователь должен знать, где и почему. + +**Совет:** Go 1.14 представила функцию [`t.Cleanup`], которую можно использовать +для регистрации функций очистки, которые запускаются при завершении вашего +теста. Функция также работает с тестовыми помощниками. См. [GoTip #4: Cleaning +Up Your Tests](https://google.github.io/styleguide/go/index.html#gotip) для +рекомендаций по упрощению тестовых помощников. + +Сниппет ниже в вымышленном файле `paint_test.go` демонстрирует, как +`(*testing.T).Helper` влияет на сообщение об ошибке в тесте Go: + +```go +package paint_test + +import ( + "fmt" + "testing" +) + +func paint(color string) error { + return fmt.Errorf("no %q paint today", color) +} + +func badSetup(t *testing.T) { + // Здесь должен быть вызов t.Helper, но его нет. + if err := paint("taupe"); err != nil { + t.Fatalf("Could not paint the house under test: %v", err) // строка 15 + } +} + +func goodSetup(t *testing.T) { + t.Helper() + if err := paint("lilac"); err != nil { + t.Fatalf("Could not paint the house under test: %v", err) + } +} + +func TestBad(t *testing.T) { + badSetup(t) + // ... +} + +func TestGood(t *testing.T) { + goodSetup(t) // строка 32 + // ... +} +``` + +Вот пример вывода при запуске. Обратите внимание на выделенный текст и на то, +как он отличается: + +```text +=== RUN TestBad + paint_test.go:15: Could not paint the house under test: no "taupe" paint today +--- FAIL: TestBad (0.00s) +=== RUN TestGood + paint_test.go:32: Could not paint the house under test: no "lilac" paint today +--- FAIL: TestGood (0.00s) +FAIL +``` + +Ошибка с `paint_test.go:15` относится к строке функции настройки, которая +завершилась неудачей в `badSetup`: + +`t.Fatalf("Could not paint the house under test: %v", err)` + +Тогда как `paint_test.go:32` относится к строке теста, которая завершилась +неудачей в `TestGood`: + +`goodSetup(t)` + +Правильное использование `(*testing.T).Helper` гораздо лучше определяет +местоположение сбоя, когда: + +* вспомогательные функции растут +* вспомогательные функции вызывают другие вспомогательные функции +* количество использований вспомогательных функций в тестовых функциях растет + +**Совет:** Если вспомогательная функция вызывает `(*testing.T).Error` или +`(*testing.T).Fatal`, предоставьте некоторый контекст в строке формата, чтобы +помочь определить, что пошло не так и почему. + +**Совет:** Если ничто из того, что делает помощник, не может привести к неудаче +теста, ему не нужно вызывать `t.Helper`. Упростите его сигнатуру, удалив `t` из +списка параметров функции. + +[`t.Cleanup`]: https://pkg.go.dev/testing#T.Cleanup + +<a id="t-fatal-goroutine"></a> + +### Не вызывайте `t.Fatal` из отдельных горутин (Don't call `t.Fatal` from separate goroutines) + +Как [документировано в пакете testing](https://pkg.go.dev/testing#T), +неправильно вызывать `t.FailNow`, `t.Fatal` и т.д. из любой горутины, кроме той, +которая запускает функцию Test (или подтест). Если ваш тест запускает новые +горутины, они не должны вызывать эти функции внутри этих горутин. + +[Тестовые помощники](#test-functions) обычно не сигнализируют о сбое из новых +горутин, поэтому для них допустимо использовать `t.Fatal`. В случае сомнений +вызовите `t.Error` и вернитесь. + +```go +// Хорошо: +func TestRevEngine(t *testing.T) { + engine, err := Start() + if err != nil { + t.Fatalf("Engine failed to start: %v", err) + } + + num := 11 + var wg sync.WaitGroup + wg.Add(num) + for i := 0; i < num; i++ { + go func() { + defer wg.Done() + if err := engine.Vroom(); err != nil { + // Здесь нельзя использовать t.Fatalf. + t.Errorf("No vroom left on engine: %v", err) + return + } + if rpm := engine.Tachometer(); rpm > 1e6 { + t.Errorf("Inconceivable engine rate: %d", rpm) + } + }() + } + wg.Wait() + + if seen := engine.NumVrooms(); seen != num { + t.Errorf("engine.NumVrooms() = %d, want %d", seen, num) + } +} +``` + +Добавление `t.Parallel` к тесту или подтесту не делает небезопасным вызов +`t.Fatal`. + +Когда все вызовы API `testing` находятся в [тестовой функции](#test-functions), +обычно легко заметить неправильное использование, потому что ключевое слово `go` +легко увидеть. Передача аргументов `testing.T` усложняет отслеживание такого +использования. Обычно причина передачи этих аргументов — введение тестового +помощника, и они не должны зависеть от тестируемой системы. Поэтому, если +тестовый помощник [регистрирует фатальную ошибку +теста](#test-helper-error-handling), он может и должен делать это из горутины +теста. + +<a id="t-field-names"></a> + +### Используйте имена полей в литералах структур (Use field names in struct literals) + +<a id="t-field-labels"></a> + +В табличных тестах предпочитайте указывать имена полей при инициализации +литералов структур тестовых случаев. Это полезно, когда тестовые случаи +охватывают большое вертикальное пространство (например, более 20-30 строк), +когда есть соседние поля с одинаковым типом, а также когда вы хотите опустить +поля, имеющие нулевое значение. Например: + +```go +// Хорошо: +func TestStrJoin(t *testing.T) { + tests := []struct { + slice []string + separator string + skipEmpty bool + want string + }{ + { + slice: []string{"a", "b", ""}, + separator: ",", + want: "a,b,", + }, + { + slice: []string{"a", "b", ""}, + separator: ",", + skipEmpty: true, + want: "a,b", + }, + // ... + } + // ... +} +``` + +<a id="t-common-setup-scope"></a> + +### Ограничивайте код настройки конкретными тестами (Keep setup code scoped to specific tests) + +По возможности настройка ресурсов и зависимостей должна быть максимально +ограничена конкретными тестовыми случаями. Например, учитывая функцию настройки: + +```go +// mustLoadDataSet загружает набор данных для тестов. +// +// Этот пример очень прост и легко читается. Часто реалистичная настройка более +// сложная, подверженная ошибкам и потенциально медленная. +func mustLoadDataset(t *testing.T) []byte { + t.Helper() + data, err := os.ReadFile("path/to/your/project/testdata/dataset") + + if err != nil { + t.Fatalf("Could not load dataset: %v", err) + } + return data +} +``` + +Вызовите `mustLoadDataset` явно в тестовых функциях, которые в этом нуждаются: + +```go +// Хорошо: +func TestParseData(t *testing.T) { + data := mustLoadDataset(t) + parsed, err := ParseData(data) + if err != nil { + t.Fatalf("Unexpected error parsing data: %v", err) + } + want := &DataTable{ /* ... */ } + if got := parsed; !cmp.Equal(got, want) { + t.Errorf("ParseData(data) = %v, want %v", got, want) + } +} + +func TestListContents(t *testing.T) { + data := mustLoadDataset(t) + contents, err := ListContents(data) + if err != nil { + t.Fatalf("Unexpected error listing contents: %v", err) + } + want := []string{ /* ... */ } + if got := contents; !cmp.Equal(got, want) { + t.Errorf("ListContents(data) = %v, want %v", got, want) + } +} + +func TestRegression682831(t *testing.T) { + if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { + t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) + } +} +``` + +Тестовая функция `TestRegression682831` не использует набор данных и поэтому не +вызывает `mustLoadDataset`, которая может быть медленной и подверженной сбоям: + +```go +// Плохо: +var dataset []byte + +func TestParseData(t *testing.T) { + // Как описано выше без вызова mustLoadDataset напрямую. +} + +func TestListContents(t *testing.T) { + // Как описано выше без вызова mustLoadDataset напрямую. +} + +func TestRegression682831(t *testing.T) { + if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { + t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) + } +} + +func init() { + dataset = mustLoadDataset() +} +``` + +Пользователь может захотеть запустить функцию изолированно от других и не должен +быть наказан этими факторами: + +```shell +# Нет причин для выполнения дорогой инициализации. +$ go test -run TestRegression682831 +``` + +<a id="t-custom-main"></a> + +#### Когда использовать пользовательскую точку входа `TestMain` (When to use a custom `TestMain` entrypoint) + +Если **все тесты в пакете** требуют общей настройки и **настройка требует +очистки (teardown)**, вы можете использовать [пользовательскую точку входа +testmain]. Это может произойти, если ресурс, требующийся тестовым случаям, +особенно дорог в настройке, и стоимость должна быть амортизирована. Обычно к +этому моменту вы уже убрали несвязанные тесты из набора тестов. Обычно это +используется только для [функциональных тестов (functional tests)]. + +Использование пользовательского `TestMain` **не должно быть вашим первым +выбором** из-за количества осторожности, которое требуется для правильного +использования. Сначала рассмотрите, достаточно ли решения в разделе +[*амортизация общей настройки теста*] или обычного [тестового помощника] для +ваших нужд. + +[пользовательскую точку входа testmain]: + https://golang.org/pkg/testing/#hdr-Main +[функциональных тестов (functional tests)]: + https://en.wikipedia.org/wiki/Functional_testing +[*амортизация общей настройки теста*]: #t-setup-amortization +[тестового помощника]: #t-common-setup-scope + +```go +// Хорошо: +var db *sql.DB + +func TestInsert(t *testing.T) { /* omitted */ } + +func TestSelect(t *testing.T) { /* omitted */ } + +func TestUpdate(t *testing.T) { /* omitted */ } + +func TestDelete(t *testing.T) { /* omitted */ } + +// runMain устанавливает зависимости теста и в конечном итоге выполняет тесты. +// Она определена как отдельная функция, чтобы этапы настройки могли четко +// откладывать (defer) свои шаги очистки. +func runMain(ctx context.Context, m *testing.M) (code int, err error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + d, err := setupDatabase(ctx) + if err != nil { + return 0, err + } + defer d.Close() // Явно очищаем базу данных. + db = d // db определена как переменная на уровне пакета. + + // m.Run() выполняет обычные, определенные пользователем тестовые функции. + // Любые операторы defer, которые были сделаны, будут выполнены после завершения m.Run(). + return m.Run(), nil +} + +func TestMain(m *testing.M) { + code, err := runMain(context.Background(), m) + if err != nil { + // Сообщения о сбоях должны записываться в STDERR, что и использует log.Fatal. + log.Fatal(err) + } + // ПРИМЕЧАНИЕ: операторы defer не выполняются после здесь из-за os.Exit + // завершающего процесс. + os.Exit(code) +} +``` + +В идеале тестовый случай является герметичным (hermetic) между вызовами самого +себя и между другими тестовыми случаями. + +По крайней мере, убедитесь, что отдельные тестовые случаи сбрасывают любое +глобальное состояние, которое они изменили, если они это сделали (например, если +тесты работают с внешней базой данных). + +<a id="t-setup-amortization"></a> + +#### Амортизация общей настройки теста (Amortizing common test setup) + +Использование `sync.Once` может быть уместным, хотя и не обязательно, если все +из следующего верно для общей настройки: + +* Она дорогая. +* Она применяется только к некоторым тестам. +* Она не требует очистки. + +```go +// Хорошо: +var dataset struct { + once sync.Once + data []byte + err error +} + +func mustLoadDataset(t *testing.T) []byte { + t.Helper() + dataset.once.Do(func() { + data, err := os.ReadFile("path/to/your/project/testdata/dataset") + // dataset определена как переменная на уровне пакета. + dataset.data = data + dataset.err = err + }) + if err := dataset.err; err != nil { + t.Fatalf("Could not load dataset: %v", err) + } + return dataset.data +} +``` + +Когда `mustLoadDataset` используется в нескольких тестовых функциях, ее +стоимость амортизируется: + +```go +// Хорошо: +func TestParseData(t *testing.T) { + data := mustLoadDataset(t) + + // Как описано выше. +} + +func TestListContents(t *testing.T) { + data := mustLoadDataset(t) + + // Как описано выше. +} + +func TestRegression682831(t *testing.T) { + if got, want := guessOS("zpc79.example.com"), "grhat"; got != want { + t.Errorf(`guessOS("zpc79.example.com") = %q, want %q`, got, want) + } +} +``` + +Причина, по которой общая очистка сложна, заключается в том, что нет единого +места для регистрации процедур очистки. Если функция настройки (в данном случае +`mustLoadDataset`) полагается на контекст, `sync.Once` может быть +проблематичным. Это потому, что второй из двух конкурентных вызовов функции +настройки должен будет ждать завершения первого вызова, прежде чем вернуться. +Этот период ожидания нельзя легко заставить уважать отмену контекста. + +<a id="string-concat"></a> + +## Конкатенация строк (String concatenation) + +Есть несколько способов конкатенации строк в Go. Некоторые примеры включают: + +* Оператор "+" +* `fmt.Sprintf` +* `strings.Builder` +* `text/template` +* `safehtml/template` + +Хотя не существует универсального правила, какой выбрать, следующие рекомендации +описывают, когда каждый метод предпочтителен. + +<a id="string-concat-simple"></a> + +### Предпочитайте "+" для простых случаев (Prefer "+" for simple cases) + +Предпочитайте использовать "+" при конкатенации нескольких строк. Этот метод +синтаксически самый простой и не требует импорта. + +```go +// Хорошо: +key := "projectid: " + p +``` + +<a id="string-concat-fmt"></a> + +### Предпочитайте `fmt.Sprintf` при форматировании (Prefer `fmt.Sprintf` when formatting) + +Предпочитайте использовать `fmt.Sprintf` при построении сложной строки с +форматированием. Использование многих операторов "+" может затмить конечный +результат. + +```go +// Хорошо: +str := fmt.Sprintf("%s [%s:%d]-> %s", src, qos, mtu, dst) +``` + +```go +// Плохо: +bad := src.String() + " [" + qos.String() + ":" + strconv.Itoa(mtu) + "]-> " + dst.String() +``` + +**Лучшая практика:** Когда результатом операции построения строки является +`io.Writer`, не конструируйте временную строку с помощью `fmt.Sprintf`, чтобы +просто отправить ее в Writer. Вместо этого используйте `fmt.Fprintf`, чтобы +отправлять прямо в Writer. + +Когда форматирование еще сложнее, предпочитайте [`text/template`] или +[`safehtml/template`] по мере необходимости. + +[`text/template`]: https://pkg.go.dev/text/template +[`safehtml/template`]: https://pkg.go.dev/github.com/google/safehtml/template + +<a id="string-concat-piecemeal"></a> + +### Предпочитайте `strings.Builder` для построения строки по частям (Prefer `strings.Builder` for constructing a string piecemeal) + +Предпочитайте использовать `strings.Builder` при построении строки по частям. +`strings.Builder` занимает амортизированное линейное время, тогда как "+" и +`fmt.Sprintf` занимают квадратичное время при последовательном вызове для +формирования большей строки. + +```go +// Хорошо: +b := new(strings.Builder) +for i, d := range digitsOfPi { + fmt.Fprintf(b, "the %d digit of pi is: %d\n", i, d) +} +str := b.String() +``` + +**Примечание:** Для более подробного обсуждения см. [GoTip #29: Building +Strings Efficiently](https://google.github.io/styleguide/go/index.html#gotip). + +<a id="string-constants"></a> + +### Константные строки (Constant strings) + +Предпочитайте использовать обратные кавычки (\`) при создании константных, +многострочных строковых литералов. + +```go +// Хорошо: +usage := `Usage: + +custom_tool [args]` +``` + +```go +// Плохо: +usage := "" + + "Usage:\n" + + "\n" + + "custom_tool [args]" +``` + +<a id="globals"></a> + +## Глобальное состояние (Global state) + +Библиотеки не должны заставлять своих клиентов использовать API, которые +полагаются на [глобальное состояние (global +state)](https://en.wikipedia.org/wiki/Global_variable). Им рекомендуется не +раскрывать API или экспортировать переменные на [уровне пакета (package level)], +которые контролируют поведение для всех клиентов как часть их API. В остальной +части раздела "глобальное" и "состояние на уровне пакета" используются как +синонимы. + +Вместо этого, если ваша функциональность поддерживает состояние, позвольте вашим +клиентам создавать и использовать экземпляры значений. + +**Важно:** Хотя это руководство применимо ко всем разработчикам, оно наиболее +критично для поставщиков инфраструктуры, которые предлагают библиотеки, +интеграции и сервисы другим командам. + +[глобальное состояние (global state)]: + https://en.wikipedia.org/wiki/Global_variable +[уровне пакета (package level)]: https://go.dev/ref/spec#TopLevelDecl + +```go +// Хорошо: +// Пакет sidecar управляет подпроцессами, которые предоставляют функции для приложений. +package sidecar + +type Registry struct { plugins map[string]*Plugin } + +func New() *Registry { return &Registry{plugins: make(map[string]*Plugin)} } + +func (r *Registry) Register(name string, p *Plugin) error { ... } +``` + +Ваши пользователи будут создавать необходимые им данные (`*sidecar.Registry`), а +затем передавать их как явную зависимость: + +```go +// Хорошо: +package main + +func main() { + sidecars := sidecar.New() + if err := sidecars.Register("Cloud Logger", cloudlogger.New()); err != nil { + log.Exitf("Could not setup cloud logger: %v", err) + } + cfg := &myapp.Config{Sidecars: sidecars} + myapp.Run(context.Background(), cfg) +} +``` + +Существуют разные подходы к миграции существующего кода для поддержки передачи +зависимостей. Основной, который вы будете использовать, — передача зависимостей +в качестве параметров конструкторам, функциям, методам или полям структур в +цепочке вызовов. + +См. также: + +* [Go Tip #5: Slimming Your Client + Libraries](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #24: Use Case-Specific + Constructions](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #40: Improving Time Testability with Function + Parameters](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #41: Identify Function Call + Parameters](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #44: Improving Time Testability with Struct + Fields](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #80: Dependency Injection + Principles](https://google.github.io/styleguide/go/index.html#gotip) + +API, которые не поддерживают явную передачу зависимостей, становятся хрупкими с +увеличением числа клиентов: + +```go +// Плохо: +package sidecar + +var registry = make(map[string]*Plugin) + +func Register(name string, p *Plugin) error { /* регистрирует плагин в registry */ } +``` + +Рассмотрим, что происходит в случае тестов, проверяющих код, который транзитивно +зависит от sidecar для облачного логирования. + +```go +// Плохо: +package app + +import ( + "cloudlogger" + "sidecar" + "testing" +) + +func TestEndToEnd(t *testing.T) { + // Система под тестом (SUT) полагается на sidecar для production облачного + // логгера, который уже зарегистрирован. + ... // Проверяем SUT и проверяем инварианты. +} + +func TestRegression_NetworkUnavailability(t *testing.T) { + // У нас был сбой из-за сетевого раздела, который сделал облачный логгер + // неработоспособным, поэтому мы добавили регрессионный тест для проверки SUT с + // тестовым двойником, имитирующим недоступность сети для логгера. + sidecar.Register("cloudlogger", cloudloggertest.UnavailableLogger) + ... // Проверяем SUT и проверяем инварианты. +} + +func TestRegression_InvalidUser(t *testing.T) { + // Система под тестом (SUT) полагается на sidecar для production облачного + // логгера, который уже зарегистрирован. + // + // Упс. cloudloggertest.UnavailableLogger все еще зарегистрирован с + // предыдущего теста. + ... // Проверяем SUT и проверяем инварианты. +} +``` + +Тесты Go выполняются последовательно по умолчанию, поэтому вышеуказанные тесты +выполняются как: + +1. `TestEndToEnd` +2. `TestRegression_NetworkUnavailability`, который переопределяет значение по + умолчанию cloudlogger +3. `TestRegression_InvalidUser`, который требует значения по умолчанию + cloudlogger, зарегистрированного в `package sidecar` + +Это создает тестовый случай, зависящий от порядка, что нарушает запуск с +фильтрами тестов и не позволяет тестам запускаться параллельно или +шардироваться. + +Использование глобального состояния создает проблемы, на которые нет простых +ответов для вас и клиентов API: + +* Что произойдет, если клиенту нужно использовать разные и отдельно работающие + наборы `Plugin` (например, для поддержки нескольких серверов) в одном + процессе? + +* Что произойдет, если клиент захочет заменить зарегистрированный `Plugin` + альтернативной реализацией в тесте, например, [тестовым двойником]? + + Что произойдет, если тестам клиента требуется герметичность между + экземплярами `Plugin` или между всеми зарегистрированными плагинами? + +* Что произойдет, если несколько клиентов `Register` плагин `Plugin` под одним + и тем же именем? Кто победит, если вообще победит? + + Как следует [обрабатывать](https://neonxp.ru/pages/gostyleguide/google/decisions/#handle-errors) ошибки? Если код + вызывает panic или `log.Fatal`, будет ли это всегда [уместно для всех мест, + в которых может быть вызван API](https://neonxp.ru/pages/gostyleguide/google/decisions/#dont-panic)? Может ли клиент + проверить, что он не делает ничего плохого, прежде чем сделать это? + +* Существуют ли определенные этапы начальной загрузки программы или ее + жизненного цикла, во время которых можно вызывать `Register`, а когда нет? + + Что произойдет, если `Register` будет вызван в неподходящее время? Клиент + может вызвать `Register` в [`func + init`](https://go.dev/ref/spec#Package_initialization), до разбора флагов + или после `main`. Этап, на котором вызывается функция, влияет на обработку + ошибок. Если автор API предполагает, что API вызывается *только* во время + инициализации программы без требования, чтобы это было так, это + предположение может подтолкнуть автора к проектированию обработки ошибок для + [завершения программы](https://neonxp.ru/pages/gostyleguide/google/best-practices/#program-init), моделируя API как + функцию типа `Must`. Завершение не подходит для библиотечных функций общего + назначения, которые могут использоваться на любом этапе. + +* Что, если потребности в параллелизме клиента и дизайнера не совпадают? + +См. также: + +* [Go Tip #36: Enclosing Package-Level + State](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #71: Reducing Parallel Test + Flakiness](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #80: Dependency Injection + Principles](https://google.github.io/styleguide/go/index.html#gotip) +* Обработка ошибок: [Look Before You + Leap](https://docs.python.org/3/glossary.html#term-LBYL) против [Easier to + Ask for Forgiveness than + Permission](https://docs.python.org/3/glossary.html#term-EAFP) +* [Unit Testing Practices on Public APIs] + +Глобальное состояние имеет каскадные эффекты на [здоровье кодовой базы +Google](https://neonxp.ru/pages/gostyleguide/google/guide/.md#maintainability). К глобальному состоянию следует подходить с +**крайней тщательностью**. + +[Глобальное состояние бывает нескольких форм](#globals-forms), и вы можете +использовать несколько [лакмусовых тестов, чтобы определить, когда оно +безопасно](#globals-litmus-tests). + +[Unit Testing Practices on Public APIs]: index.md#unit-testing-practices + +<a id="globals-forms"></a> + +### Основные формы API состояния пакета (Major forms of package state APIs) + +Ниже перечислены несколько наиболее распространенных проблемных форм API: + +* Переменные верхнего уровня, независимо от того, экспортируются они или нет. + + ```go + // Плохо: + package logger + + // Sinks управляет выходными источниками по умолчанию для API логирования этого пакета. + // Эта переменная должна быть установлена во время инициализации пакета и никогда после этого. + var Sinks []Sink + ``` + + См. [лакмусовые тесты](#globals-litmus-tests), чтобы узнать, когда они + безопасны. + +* Шаблон [локатора служб (service locator + pattern)](https://en.wikipedia.org/wiki/Service_locator_pattern). См. + [первый пример](#globals). Сам шаблон локатора служб не является + проблематичным, а проблема в том, что локатор определен как глобальный. + +* Реестры для [обратных вызовов + (callbacks)](https://en.wikipedia.org/wiki/Callback_\(computer_programming\)) + и подобного поведения. + + ```go + // Плохо: + package health + + var unhealthyFuncs []func + + func OnUnhealthy(f func()) { + unhealthyFuncs = append(unhealthyFuncs, f) + } + ``` + +* "Толстые" (thick) клиентские синглтоны для таких вещей, как бэкенды, + хранилища, уровни доступа к данным и другие системные ресурсы. Они часто + создают дополнительные проблемы с надежностью служб. + + ```go + // Плохо: + package useradmin + + var client pb.UserAdminServiceClientInterface + + func Client() *pb.UserAdminServiceClient { + if client == nil { + client = ... // Настройка клиента. + } + return client + } + ``` + +> **Примечание:** Многие устаревшие API в кодовой базе Google не следуют этому +> руководству; фактически, некоторые стандартные библиотеки Go позволяют +> настраивать поведение через глобальные значения. Тем не менее, нарушение +> этого руководства устаревшим API **[не должно использоваться как +> прецедент](https://neonxp.ru/pages/gostyleguide/google/guide/#local-consistency)** для продолжения шаблона. +> +> Лучше инвестировать в правильный дизайн API сегодня, чем платить за его +> перепроектирование позже. + +<a id="globals-litmus-tests"></a> + +### Лакмусовые тесты (Litmus tests) + +[API, использующие шаблоны выше](#globals-forms), небезопасны, когда: + +* Несколько функций взаимодействуют через глобальное состояние при выполнении + в одной программе, несмотря на то, что в остальном они независимы (например, + написаны разными авторами в совершенно разных каталогах). +* Независимые тестовые случаи взаимодействуют друг с другом через глобальное + состояние. +* Пользователи API склонны заменять или подменять глобальное состояние для + целей тестирования, особенно чтобы заменить любую часть состояния [тестовым + двойником], например, заглушкой, фейком, шпионом или моком. +* Пользователи должны учитывать особые требования к порядку при взаимодействии + с глобальным состоянием: `func init`, разобраны ли уже флаги и т.д. + +При условии, что вышеуказанные условия избегаются, существует **несколько +ограниченных обстоятельств, при которых эти API безопасны**, а именно, когда +верно любое из следующего: + +* Глобальное состояние логически постоянно + ([пример](https://github.com/klauspost/compress/blob/290f4cfacb3eff892555a491e3eeb569a48665e7/zstd/snappy.go#L413)). +* Наблюдаемое поведение пакета является бессостоятельным (stateless). + Например, общедоступная функция может использовать частную глобальную + переменную в качестве кэша, но пока вызывающая сторона не может отличить + попадания в кэш от промахов, функция является бессостоятельной. +* Глобальное состояние не просачивается в вещи, внешние по отношению к + программе, такие как sidecar-процессы или файлы в общей файловой системе. +* Нет ожидания предсказуемого поведения + ([пример](https://pkg.go.dev/math/rand)). + +> **Примечание:** +> [Sidecar-процессы](https://www.oreilly.com/library/view/designing-distributed-systems/9781491983638/ch02.html) +> могут **не** быть строго локальными для процесса. Они могут и часто +> используются совместно более чем одним процессом приложения. Более того, эти +> sidecar часто взаимодействуют с внешними распределенными системами. +> +> Кроме того, те же правила бессостоятельности, идемпотентности и локальности в +> дополнение к базовым соображениям выше применялись бы к коду самого +> sidecar-процесса! + +Пример одной из таких безопасных ситуаций — [`package +image`](https://pkg.go.dev/image) с его функцией +[`image.RegisterFormat`](https://pkg.go.dev/image#RegisterFormat). Рассмотрим +лакмусовые тесты, примененные к типичному декодеру, например, для обработки +формата [PNG](https://pkg.go.dev/image/png): + +* Множественные вызовы API `package image`, использующие зарегистрированные + декодеры (например, `image.Decode`), не могут мешать друг другу, аналогично + и для тестов. Единственное исключение — `image.RegisterFormat`, но это + смягчается пунктами ниже. +* Крайне маловероятно, что пользователь захочет заменить декодер [тестовым + двойником], так как декодер PNG является примером случая, когда предпочтение + нашей кодовой базы реальным объектам применяется. Однако пользователь с + большей вероятностью заменит декодер тестовым двойником, если декодер + состоятельно взаимодействует с ресурсами операционной системы (например, + сетью). +* Коллизии при регистрации возможны, хотя на практике они, вероятно, редки. +* Декодеры являются бессостоятельными, идемпотентными и чистыми (pure). + +<a id="globals-default-instance"></a> + +### Предоставление экземпляра по умолчанию (Providing a default instance) + +Хотя и не рекомендуется, допустимо предоставить упрощенный API, использующий +состояние на уровне пакета, если вам нужно максимизировать удобство для +пользователя. + +Следуйте [лакмусовым тестам](#globals-litmus-tests) с этими рекомендациями в +таких случаях: + +1. Пакет должен предлагать клиентам возможность создавать изолированные + экземпляры типов пакета, как [описано выше](#globals-forms). +2. Общедоступные API, использующие глобальное состояние, должны быть тонкой + прослойкой (thin proxy) к предыдущему API. Хороший пример этого — + [`http.Handle`](https://pkg.go.dev/net/http#Handle), внутренне вызывающий + [`(*http.ServeMux).Handle`](https://pkg.go.dev/net/http#ServeMux.Handle) на + переменной пакета + [`http.DefaultServeMux`](https://pkg.go.dev/net/http#DefaultServeMux). +3. Этот API уровня пакета должен использоваться только [целями сборки + бинарников (binary build targets)], а не [библиотеками (libraries)], если + только библиотеки не предпринимают рефакторинг для поддержки передачи + зависимостей. Инфраструктурные библиотеки, которые могут быть импортированы + другими пакетами, не должны полагаться на состояние на уровне пакета + импортируемых ими пакетов. + + Например, поставщик инфраструктуры, реализующий sidecar, который должен + использоваться совместно с другими командами, использующими API сверху, + должен предложить API для этого: + + ```go + // Хорошо: + package cloudlogger + + func New() *Logger { ... } + + func Register(r *sidecar.Registry, l *Logger) { + r.Register("Cloud Logging", l) + } + ``` + +4. Этот API уровня пакета должен [документировать](#documentation-conventions) + и обеспечивать соблюдение своих инвариантов (например, на каком этапе + жизненного цикла программы его можно вызывать, можно ли использовать его + параллельно). Кроме того, он должен предоставлять API для сброса глобального + состояния к известному хорошему значению по умолчанию (например, для + облегчения тестирования). + +[целями сборки бинарников (binary build targets)]: + https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_binary +[библиотеками (libraries)]: + https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_library + +См. также: + +* [Go Tip #36: Enclosing Package-Level + State](https://google.github.io/styleguide/go/index.html#gotip) +* [Go Tip #80: Dependency Injection + Principles](https://google.github.io/styleguide/go/index.html#gotip) |
