Skip to content

Latest commit

 

History

History
2318 lines (1716 loc) · 61.3 KB

style.md

File metadata and controls

2318 lines (1716 loc) · 61.3 KB

Методические рекомендации по написанию Go кода от Uber

Дисклеймер

Для более полного и глубокого понимания рекомендуется читать параллельно с английской версией.

Содержание

Введение

Стили - это соглашения, определяющие качество нашего кода. Термин стиль является не слишком полным, так как данное соглашение описывает гораздо больше, чем просто форматирование исходного кода программы, c которым и так прекрасно справляется gofmt.

Целью данного руководства является упрощение понимания, того как как можно и нужно, а как нельзя писать код на Go в Uber. Эти правила необходимы для того, чтобы сохранить контроль над кодовой базой проекта и при этом позволить программистам эффективно использовать возможности языка Go.

Данное руководство было создано [Прашантом Варанаси] и [Саймоном Ньютоном] как способ помочь коллегам начать использовать Go. С течением времени в него были внесены изменения на основе обратной связи от читателей.

Данный документ является соглашением, которому мы следуем в Uber. Многое из этого является общими рекомендациями для написания кода на Go, в то время как некоторые вещи были почерпнуты из внешних источников:

  1. Эффективный Go
  2. Руководство по распространенным ошибкам в Go

Код не должен содержать ошибок при запуске golint и go vet. Мы рекомендуем настроить ваш редактор на:

  • Запуск goimports во время сохранения
  • Запуск golint и go vet для проверки на ошибки

Информацию по поддержке Go инструментов вашим редактором можно найти здесь: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

Методические указания

Указатели на интерфейсы

Вам практически никогда не понадобится указатель на интерфейс. Интерфейсы необходимо передавать по значению, в то время как данные интерфейсов могут содержать в себе указатель.

Интерфейс содержит в себе два поля:

  1. Указатель на type-specific информацию. Можете принять это поле как "тип".
  2. Указатель на данные. Если поле содержит указатель, то он сохраняется напрямую. Если поле содержит значение, то сохраняется указатель на это значение.

Если вы хотите, чтобы интерфейс мог изменять данные, то вам необходимо использовать указатель.

Получатели и интерфейсы

Методы с получателями по значению могут также вызываться указателями.

Например,

type S struct {
  data string
}

func (s S) Read() string {
  return s.data
}

func (s *S) Write(str string) {
  s.data = str
}

sVals := map[int]S{1: {"A"}}

// You can only call Read using a value
sVals[1].Read()

// This will not compile:
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// You can call both Read and Write using a pointer
sPtrs[1].Read()
sPtrs[1].Write("test")

Аналогично, интерфейс может быть имплементирован указателем, даже если получатель метода передан по указателю.

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
//   i = s2Val

Effective Go has a good write up on Pointers vs. Values.

Zero-value Mutexes are Valid

The zero-value of sync.Mutex and sync.RWMutex is valid, so you almost never need a pointer to a mutex.

BadGood
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()

If you use a struct by pointer, then the mutex can be a non-pointer field.

Unexported structs that use a mutex to protect fields of the struct may embed the mutex.

type smap struct {
  sync.Mutex // only for unexported types

  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]
}
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]
}
Embed for private types or types that need to implement the Mutex interface. For exported types, use a private field.

Копирование срезов и мапов на границах

Срезы и мапы хранят указатели на содержащиеся в них данные, так что будьте осторожны в тех ситуациях, когда вам необходимо их копировать.

Получение срезов и мапов

Помните, что пользователи в дальнейшем могут изменить мапу или слайс, которую вы получили в качестве аргумента. Поэтому при сохранении мапы или слайса необходимо пользоваться copy().

Плохо Хорошо
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// Did you mean to modify d1.trips?
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

trips := ...
d1.SetTrips(trips)

// We can now modify trips[0] without affecting d1.trips.
trips[0] = ...

Возврат слайсов или мап

Аналогично, имейте ввиду, что пользователи смогут изменить содержимое возвращаемой внутренней мапы или слайса.

ПлохоХорошо
type Stats struct {
  mu sync.Mutex
  counters map[string]int
}

// Snapshot returns the current stats.
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  return s.counters
}

// snapshot is no longer protected by the mutex, so any
// access to the snapshot is subject to data races.
snapshot := stats.Snapshot()
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 is now a copy.
snapshot := stats.Snapshot()

Используйте Defer для освобождения ресурсов

Используйте defer для освобождения ресурсов, таких как файлы и блокировки.

ПлохоХорошо
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// легко потерять unlock'и из-за множественного return
p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// более читаемо

Defer has an extremely small overhead and should be avoided only if you can prove that your function execution time is in the order of nanoseconds. The readability win of using defers is worth the miniscule cost of using them. This is especially true for larger methods that have more than simple memory accesses, where the other computations are more significant than the defer.

Channel Size is One or None

Channels should usually have a size of one or be unbuffered. By default, channels are unbuffered and have a size of zero. Any other size must be subject to a high level of scrutiny. Consider how the size is determined, what prevents the channel from filling up under load and blocking writers, and what happens when this occurs.

BadGood
// Ought to be enough for anybody!
c := make(chan int, 64)
// Size of one
c := make(chan int, 1) // or
// Unbuffered channel, size of zero
c := make(chan int)

Начинайте перечисления (Enums) с единицы

Стандартный путь объявления перечислений в Go начинается с создания кастомного типа и группы const при помощи iota. Так как значением по умолчанию для переменных является 0, необходимо вводить перечисления, начинающихся с ненулевого значения, например с 1.

ПлохоХорошо
type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

// Add=1, Subtract=2, Multiply=3

Существуют случаи, когда использование нулевого значения имеет смысл, например, в тех ситуациях, когда нулевое значение является желаемым значением по умолчанию.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

Типизация ошибок

Существует несколько вариантов создания ошибок:

  • errors.New для ошибок с простой статичной строкой
  • fmt.Errorf для ошибок с форматируемой строкой
  • Пользовательские типы ошибок, которые имплементируют метод Error()
  • Обернутые ошибки при помощи "pkg/errors".Wrap

Во время возвращения ошибок, учтите следующие пункты, для выбора наиболее оптимального решения:

  • Возвращается простая ошибка, которая не несет в себе дополнительной информации? errors.New будет подходящим выбором.
  • Клиентам необходимо получать и обрабатывать эту ошибку? Тогда необходимо использовать кастомный тип и имплементировать метод Error().
  • Передаете ошибку из функции, которая расположена ниже по стеку вызовов? Тогда обратите внимание на section on error wrapping.
  • Во всех остальных случаях fmt.Errorf будет хорошим выбором.

Если клиенту необходимо определять ошибку и вы создали простую ошибку при помощи errors.New, используйте var для инициализации ошибки.

ПлохоХорошо
// package foo

func Open() error {
  return errors.New("could not open")
}

// package bar

func use() {
  if err := foo.Open(); err != nil {
    if err.Error() == "could not open" {
      // handle
    } else {
      panic("unknown error")
    }
  }
}
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
  return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
  if err == foo.ErrCouldNotOpen {
    // handle
  } else {
    panic("unknown error")
  }
}

Если у вас ошибка, с которой могут работать клиенты и вы хотели бы добавить больше информации к ней, тогда необходимо использовать пользовательский тип.

ПлохоХорошо
func open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

func use() {
  if err := open(); err != nil {
    if strings.Contains(err.Error(), "not found") {
      // handle
    } else {
      panic("unknown error")
    }
  }
}
type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func open(file string) error {
  return errNotFound{file: file}
}

func use() {
  if err := open(); err != nil {
    if _, ok := err.(errNotFound); ok {
      // handle
    } else {
      panic("unknown error")
    }
  }
}

Будьте осторожны с экспортом пользовательских ошибок, так как они становятся частью публичного API пакета. Желательно экспортировать функцию, которая в свою очередь проверяет ошибку.

// package foo

type errNotFound struct {
  file string
}

func (e errNotFound) Error() string {
  return fmt.Sprintf("file %q not found", e.file)
}

func IsNotFoundError(err error) bool {
  _, ok := err.(errNotFound)
  return ok
}

func Open(file string) error {
  return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
  if foo.IsNotFoundError(err) {
    // handle
  } else {
    panic("unknown error")
  }
}

Error Wrapping

There are three main options for propagating errors if a call fails:

  • Return the original error if there is no additional context to add and you want to maintain the original error type.
  • Add context using "pkg/errors".Wrap so that the error message provides more context and "pkg/errors".Cause can be used to extract the original error.
  • Use fmt.Errorf if the callers do not need to detect or handle that specific error case.

It is recommended to add context where possible so that instead of a vague error such as "connection refused", you get more useful errors such as "call service foo: connection refused".

When adding context to returned errors, keep the context succinct by avoiding phrases like "failed to", which state the obvious and pile up as the error percolates up through the stack:

BadGood
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %s", err)
}
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %s", err)
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error

However once the error is sent to another system, it should be clear the message is an error (e.g. an err tag or "Failed" prefix in logs).

See also Don't just check errors, handle them gracefully.

Handle Type Assertion Failures

The single return value form of a type assertion will panic on an incorrect type. Therefore, always use the "comma ok" idiom.

BadGood
t := i.(string)
t, ok := i.(string)
if !ok {
  // handle the error gracefully
}

Не паниковать

Код, запущенный в продакшене должен избегать паник. Паники являются главным источником cascading failures. Если вызываемая функция закончилась ошибкой, ее необходимо вернуть выше и позволить вызывающей функции решить, как ее обработать.

ПлохоХорошо
func foo(bar string) {
  if len(bar) == 0 {
    panic("bar must not be empty")
  }
  // ...
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  foo(os.Args[1])
}
func foo(bar string) error {
  if len(bar) == 0 {
    return errors.New("bar must not be empty")
  }
  // ...
  return nil
}

func main() {
  if len(os.Args) != 2 {
    fmt.Println("USAGE: foo <bar>")
    os.Exit(1)
  }
  if err := foo(os.Args[1]); err != nil {
    panic(err)
  }
}

Panic/recover не подходит в качестве механизма обработки ошибок. Программа должна паниковать только в безвыходных ситуациях, например разыменование структуры в nil. Исключением является инициализация программы: ошибки на старте могут прервать ее выполнение, вызвав панику.

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

Even in tests, prefer t.Fatal or t.FailNow over panics to ensure that the test is marked as failed.

BadGood
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  panic("failed to set up test")
}
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}

Use go.uber.org/atomic

Atomic operations with the sync/atomic package operate on the raw types (int32, int64, etc.) so it is easy to forget to use the atomic operation to read or modify the variables.

go.uber.org/atomic adds type safety to these operations by hiding the underlying type. Additionally, it includes a convenient atomic.Bool type.

BadGood
type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // already running…
     return
  }
  // start the Foo
}

func (f *foo) isRunning() bool {
  return f.running == 1  // race!
}
type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // already running…
     return
  }
  // start the Foo
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}

Производительность

Performance-specific guidelines apply only to the hot path.

Используйте strconv вместо fmt

При конвертации типов в/из строки, strconv быстрее, чем fmt.

ПлохоХорошо
for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}
for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}
BenchmarkFmtSprint-4    143 ns/op    2 allocs/op
BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

Избегайте приведения string-to-byte

Не приводите строку в слайс байтов много раз. Вместо этого выполните преобразование один раз и сохраните результат.

ПлохоХорошо
for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}
BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op

Старайтесь определять Capacity для мапов

Где возможно, старайтесь объявлять значение capacity при инициализации мап при помощи make().

make(map[T1]T2, hint)

Задание capacity во время вызова make() выделяет память для заранее известного количества элементов, что уменьшает количество операций выделения памяти во время записи значений в мапу. Помните, что capacity для мап не гарантирует того, что присваивание элементов в мапу не будет выделять под них дополнительную память.

ПлохоХорошо
m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
    m[f.Name()] = f
}
files, _ := ioutil.ReadDir("./files")

m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
    m[f.Name()] = f
}

m создается без объявления capacity; возможно дополнительное выделение памяти во время присвоения значений.

m создается с объявлением capacity; возможно меньшее или отсутствие операций выделения памяти во время присвоения значений.

Style

Be Consistent

Some of the guidelines outlined in this document can be evaluated objectively; others are situational, contextual, or subjective.

Above all else, be consistent.

Consistent code is easier to maintain, is easier to rationalize, requires less cognitive overhead, and is easier to migrate or update as new conventions emerge or classes of bugs are fixed.

Conversely, having multiple disparate or conflicting styles within a single codebase causes maintenance overhead, uncertainty, and cognitive dissonance, all of which can directly contribute to lower velocity, painful code reviews, and bugs.

When applying these guidelines to a codebase, it is recommended that changes are made at a package (or larger) level: application at a sub-package level violates the above concern by introducing multiple styles into the same code.

Группируйте похожие объявления

Go поддерживает группировку похожих объявлений.

ПлохоХорошо
import "a"
import "b"
import (
  "a"
  "b"
)

Это также применимо к константам, переменным и объявлениям типов.

ПлохоХорошо
const a = 1
const b = 2



var a = 1
var b = 2



type Area float64
type Volume float64
const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

Группируйте только близкие по смыслу объявления. Не следует группировать все подряд.

ПлохоХорошо
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  ENV_VAR = "MY_ENV"
)
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const ENV_VAR = "MY_ENV"

Группы не ограничиваются местом, где могут быть использованы. Например, вы можете использовать их внутри функций.

ПлохоХорошо
func f() string {
  var red = color.New(0xff0000)
  var green = color.New(0x00ff00)
  var blue = color.New(0x0000ff)

  ...
}
func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

Порядок импорта пакетов

Импортируемые пакеты должны быть разделены на две группы:

  • Стандартная библиотека
  • Все остальные пакеты

Такой порядок сортировки применяется утилитой goimports по умолчанию.

ПлохоХорошо
import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)
import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

Названия пакетов

При наименовании пакетов руководствуйтесь следующими принципами:

  • Название должно состоять только из символов нижнего регистра. Использовать заглавные буквы и подчеркивания запрещено.
  • Название при котором для большинства вызовов нет необходимости использовать именованный импорт.
  • Коротко и ясно. Помните, что к имени пакета необходимо обращаться при каждом вызове.
  • В единственном числе. Например, net/url вместо net/urls.
  • Не "common", "util", "shared", или "lib". Это плохие и неинформативные названия.

Также смотрите Package Names и [Style guidline for Go packages].

Названия функций

Мы следуем соглашению сообщества Go о наименовании функций MixedCaps for function names. Исключением являются функции тестов, которые могут содержать нижнее подчеркивание с целью объединения родственных тест-кейсов, например TestMyFunction_WhatIsBeingTested.

Псевдонимы импортов

Используйте псевдонимы импортов, в случае если название пакета не совпадает с последним элементом в пути импорта.

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

Во всех остальных случаях использование псевдонимов необходимо избегать, исключением является прямой конфликт в названиях импортов.

ПлохоХорошо
import (
  "fmt"
  "os"


  nettrace "golang.net/x/trace"
)
import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

Группировка и упорядочивание функций

  • Функции должны быть отсортированы в порядке приблизительного вызова.
  • Функции в файле должны быть сгруппированы по получателю.

Таким образом, экспортируемые функции должны располагаться в файле первыми, сразу после объявления struct, const и var.

Методы newXYZ()/NewXYZ() должны располагаться после определения типов, но до остальных методов получателя.

Поскольку функции сгруппированы по получателю, утилитарные функции должны располагаться в конце файла.

ПлохоХорошо
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{}
}
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 {...}

Уменьшение вложенности

Уменьшайте уровень вложенности кода, где это возможно. Старайтесь сперва обрабатывать ошибки / специальные условия и возвращать результат или continue внутри циклов. Уменьшайте количество многоуровневого вложенного кода.

ПлохоХорошо
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)
  }
}
for _, v := range data {
  if v.F1 != 1 {
    log.Printf("Invalid v: %v", v)
    continue
  }

  v = process(v)
  if err := v.Call(); err != nil {
    return err
  }
  v.Send()
}

Излишние Else

Если переменной присваивается значение в обоих ветвях if/else, то это может быть заменено единственным вызовом if.

ПлохоХорошо
var a int
if b {
  a = 100
} else {
  a = 10
}
a := 10
if b {
  a = 100
}

Объявление верхнеуровневых переменных

Для объявления верхнеуровневых переменных используйте var. Не указывайте тип, за исключением тех случаев, когда выражение не совпадает с типом.

ПлохоХорошо
var _s string = F()

func F() string { return "A" }
var _s = F()
// Поскольку F возвращает строку, нам не нужно явно указывать
// тип еще раз.

func F() string { return "A" }

Указывайте тип, если тип выражения не совпадает явно с желаемым типом.

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F возвращает объект типа myError, при этом мы хотим вернуть error.

Используйте префикс _ для глобальных неэкспортируемых переменных

Используйте префикс _ для верхнеуровневых переменных var и констант const, для явного обозначения глобальных переменных.

Исключения: Неэкспортируемые значения ошибок, которые должны быть с префиксом err.

Объяснение: Верхнеуровневые переменные и константы находятся в области видимости всего пакета. Использование общих имен может привести к случайному использованию не тех переменных в разных файлах.

ПлохоХорошо
// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)

  // Мы не увидим ошибку компиляции, если первая строка 
  // Bar() будет удалена.
}
// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

Встраивание в структуры

Встраиваемые типы (такие, как мьютексы) следует определять в самом начале списка структуры, также необходимо разделять встраиваемые поля от обычных переносом строки.

ПлохоХорошо
type Client struct {
  version int
  http.Client
}
type Client struct {
  http.Client

  version int
}

Используйте названия полей при инициализации структур

Вам практически всегда потребуется использовать названия полей при инициализации структур. Этого требует go vet.

ПлохоХорошо
k := User{"John", "Doe", true}
k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

Исключения: Названия полей могут быть опущены в тестовых таблицах, где присутствует менее 3-ех полей.

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

Определение локальных переменных

Короткое определение переменных (:=) должно использоваться в случаях если переменная определяется явным значением.

ПлохоХорошо
var s = "foo"
s := "foo"

Тем не менее, существуют случаи, когда определение через var выглядит понятнее. Declaring Empty Slices, например.

ПлохоХорошо
func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}
func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

nil это полноценный срез

nil является полноценный срезом длины 0. Это означает, что,

  • Не следует возвращать срез длины 0 явным образом. Вместо этого необходимо возвращать nil.

    BadGood
    if x == "" {
      return []int{}
    }
    if x == "" {
      return nil
    }
  • Для проверки, является ли срез пустым всегда используйте len(s) == 0. Не проверяйте его на nil.

    ПлохоХорошо
    func isEmpty(s []string) bool {
      return s == nil
    }
    func isEmpty(s []string) bool {
      return len(s) == 0
    }
  • Срез инициализированный через var сразу готов к использованию. (без make()).

    ПлохоХорошо
    nums := []int{}
    // or, nums := make([]int)
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }
    var nums []int
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }

Уменьшайте область видимости переменных

Где возможно, уменьшайте область видимости переменных, только если это не ведет к увеличению вложенности. Данное правило не должно конфликтовать с Уменьшением вложенности.

ПлохоХорошо
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
 return err
}
if err := ioutil.WriteFile(name, data, 0644); err != nil {
 return err
}

Если вам необходим результат, вызовите функцию снаружи if, тогда в этом случае нет необходимости пытаться уменьшать область видимости.

ПлохоХорошо
if data, err := ioutil.ReadFile(name); err == nil {
  err = cfg.Decode(data)
  if err != nil {
    return err
  }

  fmt.Println(cfg)
  return nil
} else {
  return err
}
data, err := ioutil.ReadFile(name)
if err != nil {
   return err
}

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil

Избегайте прямых аргументов

Прямые аргументы в функциях могут навредить читабельности. Добавляйте C-style комментарии (/* ... */) для аргументов в тех случаях, когда их значения неочевидны.

ПлохоХорошо
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)
// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

Еще лучше, если заменить типы bool кастомными типами для повышения читаемости и типо-безопасности. Также это позволит хранить и передавать для заданного параметра больше, чем два состояния (true/false).

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady = iota + 1
  StatusDone
  // Maybe we will have a StatusInProgress in the future.
)

func printInfo(name string, region Region, status Status)

Use Raw String Literals to Avoid Escaping

Go supports raw string literals, which can span multiple lines and include quotes. Use these to avoid hand-escaped strings which are much harder to read.

BadGood
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

Инициализация ссылок на структуры

Используйте &T{} вместо new(T) при инициализации ссылок на структуры, так как таким методом вы можете сразу инициализировать значения структуры.

ПлохоХорошо
sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"
sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

Инициализация мап

Используйте make(..) для пустых мапов, и мапов заполняемыми в рантайме. Это позволяет визуально отличить инициализацию мапы от ее объявления, также это позволяет в дальнейшем добавить размер мапы при инициализации в случае необходимости.

ПлохоХорошо
var (
  // m1 is safe to read and write;
  // m2 will panic on writes.
  m1 = map[T1]T2{}
  m2 map[T1]T2
)
var (
  // m1 is safe to read and write;
  // m2 will panic on writes.
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

Объявление и инициализация внешне похожи.

Объявление и инициализация внешне различаются.

Где возможно, указывайте capacity мапы при инициализации через make(). Смотрите Prefer Specifying Map Capacity Hints для более подробной информации.

С другой стороны, если мапа содержит фиксированное количество элементов используйте прямую инициализацию мапы.

ПлохоХорошо
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3
m := map[T1]T2{
  k1: v1,
  k2: v2,
  k3: v3,
}

Проще говоря, используйте явное определение мапы если заранее известно количество элементов и сами элементы, которые будут содержаться в мапе, во всех остальных случаях используйте make (также старайтесь указывать capacity)

Строки форматирования за Printf

Если вы определяете строки форматирования для Printf-style функций вне сигнатуры функции, то обозначайте их как const.

Это поможет go vet проводить статический анализ строк форматирования.

ПлохоХорошо
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

Naming Printf-style Functions

When you declare a Printf-style function, make sure that go vet can detect it and check the format string.

This means that you should use predefined Printf-style function names if possible. go vet will check these by default. See Printf family for more information.

If using the predefined names is not an option, end the name you choose with f: Wrapf, not Wrap. go vet can be asked to check specific Printf-style names but they must end with f.

$ go vet -printfuncs=wrapf,statusf

See also go vet: Printf family check.

Паттерны

Test Tables

Use table-driven tests with subtests to avoid duplicating code when the core test logic is repetitive.

BadGood
// 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)
// 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)
  })
}

Test tables make it easier to add context to error messages, reduce duplicate logic, and add new test cases.

We follow the convention that the slice of structs is referred to as tests and each test case tt. Further, we encourage explicating the input and output values for each test case with give and want prefixes.

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  // ...
}

Параметры функций (Functional Options)

Параметры функций (Functional Options) это паттерн, в котором вы определяете интерфейсный тип Option который записывает информацию в какую-то внутреннюю структуру. Вы можете принимать некоторое количество таких опций и работать со всей информацией записанной опциями во внутреннюю структуру.

Используйте данный паттерн для необязательных аргументов в конструкторах или в других методах публичных API которые будут потенциально расширяться, особенно если в этих методах уже есть три или более аргументов.

ПлохоХорошо
// package db

func Connect(
  addr string,
  timeout time.Duration,
  caching bool,
) (*Connection, error) {
  // ...
}

// Timeout and caching must always be provided,
// even if the user wants to use the default.

db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)
db.Connect(addr, newTimeout, db.DefaultCaching)
db.Connect(addr, db.DefaultTimeout, false /* caching */)
db.Connect(addr, newTimeout, false /* caching */)
type options struct {
  timeout time.Duration
  caching bool
}

// Option overrides behavior of Connect.
type Option interface {
  apply(*options)
}

type optionFunc func(*options)

func (f optionFunc) apply(o *options) {
  f(o)
}

func WithTimeout(t time.Duration) Option {
  return optionFunc(func(o *options) {
    o.timeout = t
  })
}

func WithCaching(cache bool) Option {
  return optionFunc(func(o *options) {
    o.caching = cache
  })
}

// Connect creates a connection.
func Connect(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    timeout: defaultTimeout,
    caching: defaultCaching,
  }

  for _, o := range opts {
    o.apply(&options)
  }

  // ...
}

// Options must be provided only if needed.

db.Connect(addr)
db.Connect(addr, db.WithTimeout(newTimeout))
db.Connect(addr, db.WithCaching(false))
db.Connect(
  addr,
  db.WithCaching(false),
  db.WithTimeout(newTimeout),
)

Смотрите также,