--- order: 10 title: Uber Go Style Guide --- # Руководство по стилю Uber для Go Оригинал: https://github.com/uber-go/guide/blob/master/style.md - [Введение](#введение) - [Рекомендации](#рекомендации) - [Указатели на интерфейсы](#указатели-на-интерфейсы) - [Проверка соответствия интерфейсу](#проверка-соответствия-интерфейсу) - [Получатели и интерфейсы](#получатели-и-интерфейсы) - [Нулевые значения мьютексов допустимы](#нулевые-значения-мьютексов-допустимы) - [Копируйте срезы и карты на границах](#копируйте-срезы-и-карты-на-границах) - [Используйте `defer` для очистки](#используйте-defer-для-очистки) - [Размер канала — один или ноль](#размер-канала-один-или-ноль) - [Начинайте перечисления с единицы](#начинайте-перечисления-с-единицы) - [Используйте `"time"` для работы со временем](#используйте-time-для-работы-со-временем) - [Ошибки](#ошибки) - [Типы ошибок](#типы-ошибок) - [Обёртывание ошибок](#обёртывание-ошибок) - [Именование ошибок](#именование-ошибок) - [Обрабатывайте ошибки один раз](#обрабатывайте-ошибки-один-раз) - [Обрабатывайте сбои утверждения типа](#обрабатывайте-сбои-утверждения-типа) - [Не паникуйте](#не-паникуйте) - [Используйте go.uber.org/atomic](#используйте-gouberorgatomic) - [Избегайте изменяемых глобальных переменных](#избегайте-изменяемых-глобальных-переменных) - [Избегайте встраивания типов в публичные структуры](#избегайте-встраивания-типов-в-публичные-структуры) - [Избегайте использования встроенных имён](#избегайте-использования-встроенных-имён) - [Избегайте `init()`](#избегайте-init) - [Завершение программы в main](#завершение-программы-в-main) - [Завершайте программу один раз](#завершайте-программу-один-раз) - [Используйте теги полей в структурах для сериализации](#используйте-теги-полей-в-структурах-для-сериализации) - [Не запускайте горутины по принципу «запустил и забыл»](#не-запускайте-горутины-по-принципу-запустил-и-забыл) - [Дожидайтесь завершения горутин](#дожидайтесь-завершения-горутин) - [Не используйте горутины в `init()`](#не-используйте-горутины-в-init) - [Производительность](#производительность) - [Предпочитайте strconv вместо fmt](#предпочитайте-strconv-вместо-fmt) - [Избегайте повторного преобразования строк в байты](#избегайте-повторного-преобразования-строк-в-байты) - [Предпочитайте указание ёмкости контейнеров](#предпочитайте-указание-ёмкости-контейнеров) - [Стиль](#стиль) - [Избегайте слишком длинных строк](#избегайте-слишком-длинных-строк) - [Будьте последовательны](#будьте-последовательны) - [Группируйте схожие объявления](#группируйте-схожие-объявления) - [Порядок групп импорта](#порядок-групп-импорта) - [Имена пакетов](#имена-пакетов) - [Имена функций](#имена-функций) - [Псевдонимы импорта](#псевдонимы-импорта) - [Группировка и порядок функций](#группировка-и-порядок-функций) - [Уменьшайте вложенность](#уменьшайте-вложенность) - [Избыточный Else](#избыточный-else) - [Объявления переменных верхнего уровня](#объявления-переменных-верхнего-уровня) - [Префикс \_ для неэкспортируемых глобальных переменных](#префикс-_-для-неэкспортируемых-глобальных-переменных) - [Встраивание в структурах](#встраивание-в-структурах) - [Объявление локальных переменных](#объявление-локальных-переменных) - [nil — это валидный срез](#nil-это-валидный-срез) - [Уменьшайте область видимости переменных](#уменьшайте-область-видимости-переменных) - [Избегайте «голых» параметров](#избегайте-голых-параметров) - [Используйте сырые строковые литералы, чтобы избежать экранирования](#используйте-сырые-строковые-литералы-чтобы-избежать-экранирования) - [Инициализация структур](#инициализация-структур) - [Используйте имена полей для инициализации структур](#используйте-имена-полей-для-инициализации-структур) - [Опускайте поля с нулевыми значениями в структурах](#опускайте-поля-с-нулевыми-значениями-в-структурах) - [Используйте `var` для структур с нулевыми значениями](#используйте-var-для-структур-с-нулевыми-значениями) - [Инициализация ссылок на структуры](#инициализация-ссылок-на-структуры) - [Инициализация карт](#инициализация-карт) - [Строки формата вне Printf](#строки-формата-вне-printf) - [Именование функций в стиле Printf](#именование-функций-в-стиле-printf) - [Паттерны](#паттерны) - [Табличные тесты](#табличные-тесты) - [Функциональные опции](#функциональные-опции) - [Линтинг](#линтинг) ## Введение Стили — это соглашения, которые регулируют наш код. Термин «стиль» немного неудачен, поскольку эти соглашения охватывают гораздо больше, чем просто форматирование исходных файлов — этим занимается `gofmt`. Цель этого руководства — управлять этой сложностью, подробно описывая, что следует и чего не следует делать при написании кода на Go в Uber. Эти правила существуют, чтобы кодовая база оставалась управляемой, позволяя при этом инженерам эффективно использовать возможности языка Go. Первоначально это руководство было создано [Прашантом Варанаси](https://github.com/prashantv) и [Саймоном Ньютоном](https://github.com/nomis52) как способ познакомить некоторых коллег с использованием Go. С годами оно было дополнено на основе отзывов других. В этом документе описаны идиоматические соглашения в коде на Go, которым мы следуем в Uber. Многие из них являются общими рекомендациями для Go, в то время как другие расширяют внешние ресурсы: 1. [Effective Go](https://go.dev/doc/effective_go) 2. [Go Common Mistakes](https://go.dev/wiki/CommonMistakes) 3. [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) Мы стремимся к тому, чтобы примеры кода были корректны для двух последних минорных версий [релизов Go](https://go.dev/doc/devel/release). Весь код должен быть свободен от ошибок при проверке с помощью `golint` и `go vet`. Мы рекомендуем настроить ваш редактор так, чтобы он: - Запускал `goimports` при сохранении - Запускал `golint` и `go vet` для проверки на ошибки Информацию о поддержке инструментов Go в редакторах можно найти здесь: https://go.dev/wiki/IDEsAndTextEditorPlugins ## Рекомендации ### Указатели на интерфейсы Почти никогда не нужен указатель на интерфейс. Следует передавать интерфейсы как значения — базовые данные при этом всё равно могут быть указателем. Интерфейс состоит из двух полей: 1. Указатель на информацию, специфичную для типа. Можно думать об этом как о «типе». 2. Указатель на данные. Если хранимые данные являются указателем, они сохраняются напрямую. Если хранимые данные являются значением, то сохраняется указатель на это значение. Если нужно, чтобы методы интерфейса изменяли базовые данные, необходимо использовать указатель. ### Проверка соответствия интерфейсу Проверяйте соответствие интерфейсу на этапе компиляции там, где это уместно. Это включает: - Экспортируемые типы, которые должны реализовывать определённые интерфейсы как часть своего API-контракта - Экспортируемые или неэкспортируемые типы, которые являются частью коллекции типов, реализующих один и тот же интерфейс - Другие случаи, когда нарушение интерфейса приведёт к поломке пользователей
| Плохо | Хорошо |
|---|---|
| ```go type Handler struct { // ... } func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { ... } ``` | ```go type Handler struct { // ... } var _ http.Handler = (*Handler)(nil) func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { // ... } ``` |
| Плохо | Хорошо |
|---|---|
| ```go mu := new(sync.Mutex) mu.Lock() ``` | ```go var mu sync.Mutex mu.Lock() ``` |
| Плохо | Хорошо |
|---|---|
| ```go type SMap struct { sync.Mutex data map[string]string } func NewSMap() *SMap { return &SMap{ data: make(map[string]string), } } func (m *SMap) Get(k string) string { m.Lock() defer m.Unlock() return m.data[k] } ``` | ```go type SMap struct { mu sync.Mutex data map[string]string } func NewSMap() *SMap { return &SMap{ data: make(map[string]string), } } func (m *SMap) Get(k string) string { m.mu.Lock() defer m.mu.Unlock() return m.data[k] } ``` |
| Поле `Mutex`, а также методы `Lock` и `Unlock` непреднамеренно становятся частью публичного API `SMap`. | Мьютекс и его методы являются деталями реализации `SMap`, скрытыми от его вызывающих сторон. |
| Плохо | Хорошо |
|---|---|
| ```go func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := ... d1.SetTrips(trips) // Вы хотели изменить d1.trips? trips[0] = ... ``` | ```go func (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) } trips := ... d1.SetTrips(trips) // Теперь мы можем изменить trips[0], не затрагивая d1.trips. trips[0] = ... ``` |
| Плохо | Хорошо |
|---|---|
| ```go type Stats struct { mu sync.Mutex counters map[string]int } // Snapshot возвращает текущую статистику. func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() return s.counters } // snapshot больше не защищён мьютексом, поэтому любой доступ к snapshot подвержен состоянию гонки. snapshot := stats.Snapshot() ``` | ```go type Stats struct { mu sync.Mutex counters map[string]int } func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() result := make(map[string]int, len(s.counters)) for k, v := range s.counters { result[k] = v } return result } // Snapshot теперь является копией. snapshot := stats.Snapshot() ``` |
| Плохо | Хорошо |
|---|---|
| ```go p.Lock() if p.count < 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount // легко пропустить разблокировки из-за множественных возвратов ``` | ```go p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // более читаемо ``` |
| Плохо | Хорошо |
|---|---|
| ```go // Должно хватить на всех! c := make(chan int, 64) ``` | ```go // Размер один c := make(chan int, 1) // или // Небуферизированный канал, размер ноль c := make(chan int) ``` |
| Плохо | Хорошо |
|---|---|
| ```go type Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2 ``` | ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) // Add=1, Subtract=2, Multiply=3 ``` |
| Плохо | Хорошо |
|---|---|
| ```go func isActive(now, start, stop int) bool { return start <= now && now < stop } ``` | ```go func isActive(now, start, stop time.Time) bool { return (start.Before(now) || start.Equal(now)) && now.Before(stop) } ``` |
| Плохо | Хорошо |
|---|---|
| ```go func poll(delay int) { for { // ... time.Sleep(time.Duration(delay) * time.Millisecond) } } poll(10) // это секунды или миллисекунды? ``` | ```go func poll(delay time.Duration) { for { // ... time.Sleep(delay) } } poll(10*time.Second) ``` |
| Плохо | Хорошо |
|---|---|
| ```go // {"interval": 2} type Config struct { Interval int `json:"interval"` } ``` | ```go // {"intervalMillis": 2000} type Config struct { IntervalMillis int `json:"intervalMillis"` } ``` |
| Без сопоставления ошибок | С сопоставлением ошибок |
|---|---|
| ```go // package foo func Open() error { return errors.New("could not open") } // package bar if err := foo.Open(); err != nil { // Не можем обработать ошибку. panic("unknown error") } ``` | ```go // package foo var ErrCouldNotOpen = errors.New("could not open") func Open() error { return ErrCouldNotOpen } // package bar if err := foo.Open(); err != nil { if errors.Is(err, foo.ErrCouldNotOpen) { // обработать ошибку } else { panic("unknown error") } } ``` |
| Без сопоставления ошибок | С сопоставлением ошибок |
|---|---|
| ```go // package foo func Open(file string) error { return fmt.Errorf("file %q not found", file) } // package bar if err := foo.Open("testfile.txt"); err != nil { // Не можем обработать ошибку. panic("unknown error") } ``` | ```go // package foo type NotFoundError struct { File string } func (e *NotFoundError) Error() string { return fmt.Sprintf("file %q not found", e.File) } func Open(file string) error { return &NotFoundError{File: file} } // package bar if err := foo.Open("testfile.txt"); err != nil { var notFound *NotFoundError if errors.As(err, ¬Found) { // обработать ошибку } else { panic("unknown error") } } ``` |
| Плохо | Хорошо |
|---|---|
| ```go s, err := store.New() if err != nil { return fmt.Errorf( "failed to create new store: %w", err) } ``` | ```go s, err := store.New() if err != nil { return fmt.Errorf( "new store: %w", err) } ``` |
| ```plain failed to x: failed to y: failed to create new store: the error ``` | ```plain x: y: new store: the error ``` |
| Описание | Код |
|---|---|
| **Плохо**: Логировать ошибку и возвращать её Вызывающие стороны выше по стеку, вероятно, предпримут аналогичные действия с ошибкой. Это приведёт к большому количеству шума в логах приложения при малой пользе. | ```go u, err := getUser(id) if err != nil { // ПЛОХО: см. описание log.Printf("Could not get user %q: %v", id, err) return err } ``` |
| **Хорошо**: Обернуть ошибку и вернуть её Вызывающие стороны выше по стеку обработают ошибку. Использование `%w` гарантирует, что они смогут сопоставить ошибку с `errors.Is` или `errors.As`, если это уместно. | ```go u, err := getUser(id) if err != nil { return fmt.Errorf("get user %q: %w", id, err) } ``` |
| **Хорошо**: Логировать ошибку и выполнить graceful degradation Если операция не является строго необходимой, мы можем обеспечить деградировавший, но работающий опыт, восстановившись после ошибки. | ```go if err := emitMetrics(); err != nil { // Неудача при записи метрик не должна ломать приложение. log.Printf("Could not emit metrics: %v", err) } ``` |
| **Хорошо**: Сопоставить ошибку и выполнить graceful degradation Если вызываемая функция определяет конкретную ошибку в своём контракте и сбой является восстанавливаемым, сопоставьте этот случай ошибки и выполните graceful degradation. Для всех остальных случаев оберните ошибку и верните её. Вызывающие стороны выше по стеку обработают другие ошибки. | ```go tz, err := getUserTimeZone(id) if err != nil { if errors.Is(err, ErrUserNotFound) { // Пользователь не существует. Используем UTC. tz = time.UTC } else { return fmt.Errorf("get user %q: %w", id, err) } } ``` |
| Плохо | Хорошо |
|---|---|
| ```go t := i.(string) ``` | ```go t, ok := i.(string) if !ok { // обработать ошибку корректно } ``` |
| Плохо | Хорошо |
|---|---|
| ```go func run(args []string) { if len(args) == 0 { panic("an argument is required") } // ... } func main() { run(os.Args[1:]) } ``` | ```go func run(args []string) error { if len(args) == 0 { return errors.New("an argument is required") } // ... return nil } func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } ``` |
| Плохо | Хорошо |
|---|---|
| ```go // func TestFoo(t *testing.T) f, err := os.CreateTemp("", "test") if err != nil { panic("failed to set up test") } ``` | ```go // func TestFoo(t *testing.T) f, err := os.CreateTemp("", "test") if err != nil { t.Fatal("failed to set up test") } ``` |
| Плохо | Хорошо |
|---|---|
| ```go type foo struct { running int32 // atomic } func (f* foo) start() { if atomic.SwapInt32(&f.running, 1) == 1 { // уже работает… return } // запустить Foo } func (f *foo) isRunning() bool { return f.running == 1 // состояние гонки! } ``` | ```go type foo struct { running atomic.Bool } func (f *foo) start() { if f.running.Swap(true) { // уже работает… return } // запустить Foo } func (f *foo) isRunning() bool { return f.running.Load() } ``` |
| Плохо | Хорошо |
|---|---|
| ```go // sign.go var _timeNow = time.Now func sign(msg string) string { now := _timeNow() return signWithTime(msg, now) } ``` | ```go // sign.go type signer struct { now func() time.Time } func newSigner() *signer { return &signer{ now: time.Now, } } func (s *signer) Sign(msg string) string { now := s.now() return signWithTime(msg, now) } ``` |
| ```go // sign_test.go func TestSign(t *testing.T) { oldTimeNow := _timeNow _timeNow = func() time.Time { return someFixedTime } defer func() { _timeNow = oldTimeNow }() assert.Equal(t, want, sign(give)) } ``` | ```go // sign_test.go func TestSigner(t *testing.T) { s := newSigner() s.now = func() time.Time { return someFixedTime } assert.Equal(t, want, s.Sign(give)) } ``` |
| Плохо | Хорошо |
|---|---|
| ```go // ConcreteList — это список сущностей. type ConcreteList struct { *AbstractList } ``` | ```go // ConcreteList — это список сущностей. type ConcreteList struct { list *AbstractList } // Add добавляет сущность в список. func (l *ConcreteList) Add(e Entity) { l.list.Add(e) } // Remove удаляет сущность из списка. func (l *ConcreteList) Remove(e Entity) { l.list.Remove(e) } ``` |
| Плохо | Хорошо |
|---|---|
| ```go // AbstractList — это обобщённая реализация для различных видов списков сущностей. type AbstractList interface { Add(Entity) Remove(Entity) } // ConcreteList — это список сущностей. type ConcreteList struct { AbstractList } ``` | ```go // ConcreteList — это список сущностей. type ConcreteList struct { list AbstractList } // Add добавляет сущность в список. func (l *ConcreteList) Add(e Entity) { l.list.Add(e) } // Remove удаляет сущность из списка. func (l *ConcreteList) Remove(e Entity) { l.list.Remove(e) } ``` |
| Плохо | Хорошо |
|---|---|
| ```go var error string // `error` затеняет встроенный идентификатор // или func handleErrorMessage(error string) { // `error` затеняет встроенный идентификатор } ``` | ```go var errorMessage string // `error` ссылается на встроенный идентификатор // или func handleErrorMessage(msg string) { // `error` ссылается на встроенный идентификатор } ``` |
| ```go type Foo struct { // Хотя технически эти поля не создают затенение, поиск по строкам `error` или `string` теперь неоднозначен. error error string string } func (f Foo) Error() error { // `error` и `f.error` визуально похожи return f.error } func (f Foo) String() string { // `string` и `f.string` визуально похожи return f.string } ``` | ```go type Foo struct { // `error` и `string` теперь однозначны. err error str string } func (f Foo) Error() error { return f.err } func (f Foo) String() string { return f.str } ``` |
| Плохо | Хорошо |
|---|---|
| ```go type Foo struct { // ... } var _defaultFoo Foo func init() { _defaultFoo = Foo{ // ... } } ``` | ```go var _defaultFoo = Foo{ // ... } // или, лучше, для тестируемости: var _defaultFoo = defaultFoo() func defaultFoo() Foo { return Foo{ // ... } } ``` |
| ```go type Config struct { // ... } var _config Config func init() { // Плохо: зависит от текущей директории cwd, _ := os.Getwd() // Плохо: ввод-вывод raw, _ := os.ReadFile( path.Join(cwd, "config", "config.yaml"), ) yaml.Unmarshal(raw, &_config) } ``` | ```go type Config struct { // ... } func loadConfig() Config { cwd, err := os.Getwd() // обработать err raw, err := os.ReadFile( path.Join(cwd, "config", "config.yaml"), ) // обработать err var config Config yaml.Unmarshal(raw, &config) return config } ``` |
| Плохо | Хорошо |
|---|---|
| ```go func main() { body := readFile(path) fmt.Println(body) } func readFile(path string) string { f, err := os.Open(path) if err != nil { log.Fatal(err) } b, err := io.ReadAll(f) if err != nil { log.Fatal(err) } return string(b) } ``` | ```go func main() { body, err := readFile(path) if err != nil { log.Fatal(err) } fmt.Println(body) } func readFile(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } b, err := io.ReadAll(f) if err != nil { return "", err } return string(b), nil } ``` |
| Плохо | Хорошо |
|---|---|
| ```go package main func main() { args := os.Args[1:] if len(args) != 1 { log.Fatal("missing file") } name := args[0] f, err := os.Open(name) if err != nil { log.Fatal(err) } defer f.Close() // Если мы вызовем log.Fatal после этой строки, f.Close не будет вызван. b, err := io.ReadAll(f) if err != nil { log.Fatal(err) } // ... } ``` | ```go package main func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { args := os.Args[1:] if len(args) != 1 { return errors.New("missing file") } name := args[0] f, err := os.Open(name) if err != nil { return err } defer f.Close() b, err := io.ReadAll(f) if err != nil { return err } // ... } ``` |
| Плохо | Хорошо |
|---|---|
| ```go type Stock struct { Price int Name string } bytes, err := json.Marshal(Stock{ Price: 137, Name: "UBER", }) ``` | ```go type Stock struct { Price int `json:"price"` Name string `json:"name"` // Безопасно переименовать Name в Symbol. } bytes, err := json.Marshal(Stock{ Price: 137, Name: "UBER", }) ``` |
| Плохо | Хорошо |
|---|---|
| ```go go func() { for { flush() time.Sleep(delay) } }() ``` | ```go var ( stop = make(chan struct{}) // сигнализирует горутине остановиться done = make(chan struct{}) // сигнализирует нам, что горутина завершилась ) go func() { defer close(done) ticker := time.NewTicker(delay) defer ticker.Stop() for { select { case <-ticker.C: flush() case <-stop: return } } }() // В другом месте... close(stop) // сигнализировать горутине остановиться <-done // и ждать её завершения ``` |
| Нет способа остановить эту горутину. Она будет работать, пока приложение не завершится. | Эту горутину можно остановить с помощью `close(stop)`, и мы можем дождаться её завершения с помощью `<-done`. |
| Плохо | Хорошо |
|---|---|
| ```go func init() { go doWork() } func doWork() { for { // ... } } ``` | ```go type Worker struct{ /* ... */ } func NewWorker(...) *Worker { w := &Worker{ stop: make(chan struct{}), done: make(chan struct{}), // ... } go w.doWork() } func (w *Worker) doWork() { defer close(w.done) for { // ... case <-w.stop: return } } // Shutdown говорит воркеру остановиться и ждёт, пока он завершится. func (w *Worker) Shutdown() { close(w.stop) <-w.done } ``` |
| Создаёт фоновую горутину безусловно при экспорте этого пакета пользователем. Пользователь не имеет контроля над горутиной или способа её остановки. | Создаёт воркера только если пользователь его запрашивает. Предоставляет способ остановки воркера, чтобы пользователь мог освободить используемые им ресурсы. Обратите внимание, что следует использовать `WaitGroup`, если воркер управляет несколькими горутинами. См. [Дожидайтесь завершения горутин](#дожидайтесь-завершения-горутин). |
| Плохо | Хорошо |
|---|---|
| ```go for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) } ``` | ```go for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) } ``` |
| ```plain BenchmarkFmtSprint-4 143 ns/op 2 allocs/op ``` | ```plain BenchmarkStrconv-4 64.2 ns/op 1 allocs/op ``` |
| Плохо | Хорошо |
|---|---|
| ```go for i := 0; i < b.N; i++ { w.Write([]byte("Hello world")) } ``` | ```go data := []byte("Hello world") for i := 0; i < b.N; i++ { w.Write(data) } ``` |
| ```plain BenchmarkBad-4 50000000 22.2 ns/op ``` | ```plain BenchmarkGood-4 500000000 3.25 ns/op ``` |
| Плохо | Хорошо |
|---|---|
| ```go m := make(map[string]os.FileInfo) files, _ := os.ReadDir("./files") for _, f := range files { m[f.Name()] = f } ``` | ```go files, _ := os.ReadDir("./files") m := make(map[string]os.DirEntry, len(files)) for _, f := range files { m[f.Name()] = f } ``` |
| `m` создаётся без подсказки размера; при присваивании может быть больше выделений памяти. | `m` создаётся с подсказкой размера; при присваивании может быть меньше выделений памяти. |
| Плохо | Хорошо |
|---|---|
| ```go for n := 0; n < b.N; n++ { data := make([]int, 0) for k := 0; k < size; k++{ data = append(data, k) } } ``` | ```go for n := 0; n < b.N; n++ { data := make([]int, 0, size) for k := 0; k < size; k++{ data = append(data, k) } } ``` |
| ```plain BenchmarkBad-4 100000000 2.48s ``` | ```plain BenchmarkGood-4 100000000 0.21s ``` |
| Плохо | Хорошо |
|---|---|
| ```go import "a" import "b" ``` | ```go import ( "a" "b" ) ``` |
| Плохо | Хорошо |
|---|---|
| ```go const a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64 ``` | ```go const ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 ) ``` |
| Плохо | Хорошо |
|---|---|
| ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply EnvVar = "MY_ENV" ) ``` | ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const EnvVar = "MY_ENV" ``` |
| Плохо | Хорошо |
|---|---|
| ```go func f() string { red := color.New(0xff0000) green := color.New(0x00ff00) blue := color.New(0x0000ff) // ... } ``` | ```go func f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) // ... } ``` |
| Плохо | Хорошо |
|---|---|
| ```go func (c *client) request() { caller := c.name format := "json" timeout := 5*time.Second var err error // ... } ``` | ```go func (c *client) request() { var ( caller = c.name format = "json" timeout = 5*time.Second err error ) // ... } ``` |
| Плохо | Хорошо |
|---|---|
| ```go import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" ) ``` | ```go import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" ) ``` |
| Плохо | Хорошо |
|---|---|
| ```go import ( "fmt" "os" runtimetrace "runtime/trace" nettrace "golang.net/x/trace" ) ``` | ```go import ( "fmt" "os" "runtime/trace" nettrace "golang.net/x/trace" ) ``` |
| Плохо | Хорошо |
|---|---|
| ```go func (s *something) Cost() { return calcCost(s.weights) } type something struct{ ... } func calcCost(n []int) int {...} func (s *something) Stop() {...} func newSomething() *something { return &something{} } ``` | ```go type something struct{ ... } func newSomething() *something { return &something{} } func (s *something) Cost() { return calcCost(s.weights) } func (s *something) Stop() {...} func calcCost(n []int) int {...} ``` |
| Плохо | Хорошо |
|---|---|
| ```go for _, v := range data { if v.F1 == 1 { v = process(v) if err := v.Call(); err == nil { v.Send() } else { return err } } else { log.Printf("Invalid v: %v", v) } } ``` | ```go for _, v := range data { if v.F1 != 1 { log.Printf("Invalid v: %v", v) continue } v = process(v) if err := v.Call(); err != nil { return err } v.Send() } ``` |
| Плохо | Хорошо |
|---|---|
| ```go var a int if b { a = 100 } else { a = 10 } ``` | ```go a := 10 if b { a = 100 } ``` |
| Плохо | Хорошо |
|---|---|
| ```go var _s string = F() func F() string { return "A" } ``` | ```go var _s = F() // Поскольку F уже указывает, что возвращает строку, нам не нужно снова указывать тип. func F() string { return "A" } ``` |
| Плохо | Хорошо |
|---|---|
| ```go // foo.go const ( defaultPort = 8080 defaultUser = "user" ) // bar.go func Bar() { defaultPort := 9090 ... fmt.Println("Default port", defaultPort) // Мы не увидим ошибку компиляции, если первая строка Bar() будет удалена. } ``` | ```go // foo.go const ( _defaultPort = 8080 _defaultUser = "user" ) ``` |
| Плохо | Хорошо |
|---|---|
| ```go type Client struct { version int http.Client } ``` | ```go type Client struct { http.Client version int } ``` |
| Плохо | Хорошо |
|---|---|
| ```go type A struct { // Плохо: A.Lock() и A.Unlock() теперь доступны, не предоставляют функциональной пользы и позволяют пользователям контролировать детали внутренностей A. sync.Mutex } ``` | ```go type countingWriteCloser struct { // Хорошо: Write() предоставлен на этом внешнем уровне для конкретной цели и делегирует работу Write() внутреннего типа. io.WriteCloser count int } func (w *countingWriteCloser) Write(bs []byte) (int, error) { w.count += len(bs) return w.WriteCloser.Write(bs) } ``` |
| ```go type Book struct { // Плохо: указатель меняет полезность нулевого значения io.ReadWriter // другие поля } // позже var b Book b.Read(...) // panic: nil pointer b.String() // panic: nil pointer b.Write(...) // panic: nil pointer ``` | ```go type Book struct { // Хорошо: имеет полезное нулевое значение bytes.Buffer // другие поля } // позже var b Book b.Read(...) // ok b.String() // ok b.Write(...) // ok ``` |
| ```go type Client struct { sync.Mutex sync.WaitGroup bytes.Buffer url.URL } ``` | ```go type Client struct { mtx sync.Mutex wg sync.WaitGroup buf bytes.Buffer url url.URL } ``` |
| Плохо | Хорошо |
|---|---|
| ```go var s = "foo" ``` | ```go s := "foo" ``` |
| Плохо | Хорошо |
|---|---|
| ```go func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } ``` | ```go func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } ``` |
| Плохо | Хорошо |
|---|---|
| ```go if x == "" { return []int{} } ``` | ```go if x == "" { return nil } ``` |
| Плохо | Хорошо |
|---|---|
| ```go func isEmpty(s []string) bool { return s == nil } ``` | ```go func isEmpty(s []string) bool { return len(s) == 0 } ``` |
| Плохо | Хорошо |
|---|---|
| ```go nums := []int{} // или, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } ``` | ```go var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } ``` |
| Плохо | Хорошо |
|---|---|
| ```go err := os.WriteFile(name, data, 0644) if err != nil { return err } ``` | ```go if err := os.WriteFile(name, data, 0644); err != nil { return err } ``` |
| Плохо | Хорошо |
|---|---|
| ```go if data, err := os.ReadFile(name); err == nil { err = cfg.Decode(data) if err != nil { return err } fmt.Println(cfg) return nil } else { return err } ``` | ```go data, err := os.ReadFile(name) if err != nil { return err } if err := cfg.Decode(data); err != nil { return err } fmt.Println(cfg) return nil ``` |
| Плохо | Хорошо |
|---|---|
| ```go const ( _defaultPort = 8080 _defaultUser = "user" ) func Bar() { fmt.Println("Default port", _defaultPort) } ``` | ```go func Bar() { const ( defaultPort = 8080 defaultUser = "user" ) fmt.Println("Default port", defaultPort) } ``` |
| Плохо | Хорошо |
|---|---|
| ```go // func printInfo(name string, isLocal, done bool) printInfo("foo", true, true) ``` | ```go // func printInfo(name string, isLocal, done bool) printInfo("foo", true /* isLocal */, true /* done */) ``` |
| Плохо | Хорошо |
|---|---|
| ```go wantError := "unknown name:\"test\"" ``` | ```go wantError := `unknown error:"test"` ``` |
| Плохо | Хорошо |
|---|---|
| ```go k := User{"John", "Doe", true} ``` | ```go k := User{ FirstName: "John", LastName: "Doe", Admin: true, } ``` |
| Плохо | Хорошо |
|---|---|
| ```go user := User{ FirstName: "John", LastName: "Doe", MiddleName: "", Admin: false, } ``` | ```go user := User{ FirstName: "John", LastName: "Doe", } ``` |
| Плохо | Хорошо |
|---|---|
| ```go user := User{} ``` | ```go var user User ``` |
| Плохо | Хорошо |
|---|---|
| ```go sval := T{Name: "foo"} // несогласованно sptr := new(T) sptr.Name = "bar" ``` | ```go sval := T{Name: "foo"} sptr := &T{Name: "bar"} ``` |
| Плохо | Хорошо |
|---|---|
| ```go var ( // m1 безопасна для чтения и записи; // m2 вызовет панику при записи. m1 = map[T1]T2{} m2 map[T1]T2 ) ``` | ```go var ( // m1 безопасна для чтения и записи; // m2 вызовет панику при записи. m1 = make(map[T1]T2) m2 map[T1]T2 ) ``` |
| Объявление и инициализация визуально похожи. | Объявление и инициализация визуально различны. |
| Плохо | Хорошо |
|---|---|
| ```go m := make(map[T1]T2, 3) m[k1] = v1 m[k2] = v2 m[k3] = v3 ``` | ```go m := map[T1]T2{ k1: v1, k2: v2, k3: v3, } ``` |
| Плохо | Хорошо |
|---|---|
| ```go msg := "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2) ``` | ```go const msg = "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2) ``` |
| Плохо | Хорошо |
|---|---|
| ```go // func TestSplitHostPort(t *testing.T) host, port, err := net.SplitHostPort("192.0.2.0:8000") require.NoError(t, err) assert.Equal(t, "192.0.2.0", host) assert.Equal(t, "8000", port) host, port, err = net.SplitHostPort("192.0.2.0:http") require.NoError(t, err) assert.Equal(t, "192.0.2.0", host) assert.Equal(t, "http", port) host, port, err = net.SplitHostPort(":8000") require.NoError(t, err) assert.Equal(t, "", host) assert.Equal(t, "8000", port) host, port, err = net.SplitHostPort("1:8") require.NoError(t, err) assert.Equal(t, "1", host) assert.Equal(t, "8", port) ``` | ```go // func TestSplitHostPort(t *testing.T) tests := []struct{ give string wantHost string wantPort string }{ { give: "192.0.2.0:8000", wantHost: "192.0.2.0", wantPort: "8000", }, { give: "192.0.2.0:http", wantHost: "192.0.2.0", wantPort: "http", }, { give: ":8000", wantHost: "", wantPort: "8000", }, { give: "1:8", wantHost: "1", wantPort: "8", }, } for _, tt := range tests { t.Run(tt.give, func(t *testing.T) { host, port, err := net.SplitHostPort(tt.give) require.NoError(t, err) assert.Equal(t, tt.wantHost, host) assert.Equal(t, tt.wantPort, port) }) } ``` |
| Плохо | Хорошо |
|---|---|
| ```go func TestComplicatedTable(t *testing.T) { tests := []struct { give string want string wantErr error shouldCallX bool shouldCallY bool giveXResponse string giveXErr error giveYResponse string giveYErr error }{ // ... } for _, tt := range tests { t.Run(tt.give, func(t *testing.T) { // настройка моков ctrl := gomock.NewController(t) xMock := xmock.NewMockX(ctrl) if tt.shouldCallX { xMock.EXPECT().Call().Return( tt.giveXResponse, tt.giveXErr, ) } yMock := ymock.NewMockY(ctrl) if tt.shouldCallY { yMock.EXPECT().Call().Return( tt.giveYResponse, tt.giveYErr, ) } got, err := DoComplexThing(tt.give, xMock, yMock) // проверка результатов if tt.wantErr != nil { require.EqualError(t, err, tt.wantErr) return } require.NoError(t, err) assert.Equal(t, want, got) }) } } ``` | ```go func TestShouldCallX(t *testing.T) { // настройка моков ctrl := gomock.NewController(t) xMock := xmock.NewMockX(ctrl) xMock.EXPECT().Call().Return("XResponse", nil) yMock := ymock.NewMockY(ctrl) got, err := DoComplexThing("inputX", xMock, yMock) require.NoError(t, err) assert.Equal(t, "want", got) } func TestShouldCallYAndFail(t *testing.T) { // настройка моков ctrl := gomock.NewController(t) xMock := xmock.NewMockX(ctrl) yMock := ymock.NewMockY(ctrl) yMock.EXPECT().Call().Return("YResponse", nil) _, err := DoComplexThing("inputY", xMock, yMock) assert.EqualError(t, err, "Y failed") } ``` |
| Плохо | Хорошо |
|---|---|
| ```go // package db func Open( addr string, cache bool, logger *zap.Logger ) (*Connection, error) { // ... } ``` | ```go // package db type Option interface { // ... } func WithCache(c bool) Option { // ... } func WithLogger(log *zap.Logger) Option { // ... } // Open создаёт соединение. func Open( addr string, opts ...Option, ) (*Connection, error) { // ... } ``` |
| Параметры cache и logger всегда должны предоставляться, даже если пользователь хочет использовать значения по умолчанию. ```go db.Open(addr, db.DefaultCache, zap.NewNop()) db.Open(addr, db.DefaultCache, log) db.Open(addr, false /* cache */, zap.NewNop()) db.Open(addr, false /* cache */, log) ``` | Опции предоставляются только при необходимости. ```go db.Open(addr) db.Open(addr, db.WithLogger(log)) db.Open(addr, db.WithCache(false)) db.Open( addr, db.WithCache(false), db.WithLogger(log), ) ``` |