Спецификация кодирования языка Uber Go

Go

Спецификация кодирования языка Uber Go

UberЭто технологическая компания из Силиконовой долины, одна из первых внедрившая язык Go. Он открыл исходный код многих проектов golang, например, хорошо известных в кругу Gopher.zap,jaegerЖдать. В конце 2018 года UberРуководство по стилю GoОткрытый исходный код для GitHub, после года накопления и обновления спецификация начала обретать форму и привлекла внимание большинства Gophers. Эта статья является китайской версией спецификации. Эта версия будет обновляться в режиме реального времени в соответствии с исходной версией.

Версия

  • Текущая версия обновления: 2019-11-13 Адрес версии:commit:#71
  • Не стесняйтесь форкнуть и PR, если вы обнаружите какие-либо обновления, проблемы или улучшения.
  • Please feel free to fork and PR if you find any updates, issues or improvement.

содержание

вводить

Стили — это соглашения, которые управляют нашим кодом. срок样式Немного неправильное название, поскольку эти соглашения охватывают больше, чем форматы исходных файлов, которые обрабатывает для нас gofmt.

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

Это руководство было первоначально созданоPrashant VaranasiиSimon NewtonНаписано, чтобы позволить некоторым коллегам быстро использовать Go. На протяжении многих лет это руководство пересматривалось на основе отзывов других пользователей.

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

  1. Effective Go
  2. The Go common mistakes guide

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

  • запустить при сохраненииgoimports
  • бегатьgolintиgo vetпроверить на ошибки

Вы можете найти более подробную информацию на следующих страницах поддержки инструмента Go Editor:GitHub.com/gowaves/go/me…

Руководящие принципы

указатель на интерфейс

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

Интерфейс по существу представлен под капотом двумя полями:

  1. Указатель на определенный тип информации. Вы можете думать об этом как о «типе».
  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"}}

// 你只能通过值调用 Read
sVals[1].Read()

// 这不能编译通过:
//  sVals[1].Write("test")

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

// 通过指针既可以调用 Read,也可以调用 Write 方法
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

//  下面代码无法通过编译。因为 s2Val 是一个值,而 S2 的 f 方法中没有使用值接收器
//   i = s2Val

Effective GoЕсть отрывок оpointers vs. valuesпрекрасное объяснение.

Мьютекс с нулевым значением действителен

нулевое значениеsync.Mutexиsync.RWMutexЭто эффективно. Таким образом, указатели на мьютексы в основном не нужны.

Bad Good
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()

Если вы используете указатели на структуры, мьютекс может быть формой без указателя в качестве составного поля структуры или, что еще лучше, встроен непосредственно в структуру. Если это частный тип структуры или тип, реализующий интерфейс 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]
}
Внедряет частные типы или типы, которым необходимо реализовать взаимоисключающий интерфейс. Для экспортируемых типов используйте специальные поля.

Копировать срезы и карты на границах

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

Получение фрагментов и карт

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

Bad Good
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

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

// 你是要修改 d1.trips 吗?
trips[0] = ...
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] = ...

возвращает срезы или карты

Опять же, помните о пользовательских изменениях карт или срезов, которые раскрывают внутреннее состояние.

Bad Good
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 的任何访问都将受到数据竞争的影响
// 影响 stats.counters
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 现在是一个拷贝
snapshot := stats.Snapshot()

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

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

Bad Good
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

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

return newCount

// 当有多个 return 分支时,很容易遗忘 unlock
p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// 更可读

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

Размер канала либо 1, либо небуферизованный

Каналы обычно должны иметь размер 1 или не буферизоваться. По умолчанию каналы не буферизованы и их размер равен нулю. Любой другой размер должен пройти тщательную проверку. Рассмотрим, как определяется размер, что препятствует заполнению канала под нагрузкой и предотвращению записи и что при этом происходит.

Bad Good
// 应该足以满足任何情况!
c := make(chan int, 64)
// 大小:1
c := make(chan int, 1) // 或者
// 无缓冲 channel,大小为 0
c := make(chan int)

Перечисление начинается с 1

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

Bad Good
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

тип ошибки

В Go есть различные варианты объявления ошибок:

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

При возврате ошибки учитывайте следующие факторы, чтобы определить наилучший вариант:

  • Это простая ошибка, которая не требует дополнительной информации? если так,errors.Newдостаточно.

  • Должен ли клиент обнаруживать и обрабатывать эту ошибку? Если это так, вы должны использовать собственный тип и реализовать его.Error()метод.

  • Распространяете ли вы ошибки, возвращаемые нижестоящими функциями? Если это так, см. далее в этой статье об упаковке ошибок.section on error wrappingчасть контента.

  • в противном случаеfmt.ErrorfВот и все.

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

Bad Good
// 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")
  }
}

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

Bad Good
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")
  }
}

Обтекание ошибок

Существует три основных способа распространения ошибки при сбое вызова (функции/метода):

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

  • Чтобы добавить контекст, используйте"pkg/errors".Wrapчтобы сообщение об ошибке содержало больше контекста,"pkg/errors".CauseМожет использоваться для извлечения исходной ошибки. Используйте fmt.Errorf, если вызывающим объектам не нужно обнаруживать или обрабатывать этот конкретный случай ошибки.

  • Если вызывающему объекту не нужно обнаруживать или обрабатывать конкретное состояние ошибки, используйтеfmt.Errorf.

Рекомендуется добавлять контекст, где это возможно, чтобы вы получали больше полезных ошибок, таких как «вызов службы foo: соединение отклонено», а не расплывчатые ошибки, такие как «соединение отклонено».

При добавлении контекста к возвращаемым ошибкам сохраняйте краткость контекста, избегая таких фраз, как «не удалось», которые констатируют очевидное и накапливаются по мере того, как ошибка просачивается вниз по стеку:

Bad Good
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

Однако, как только ошибка отправляется в другую систему, должно быть ясно, что это сообщение является сообщением об ошибке (например, с помощьюerrили с префиксом «Ошибка» в журналах).

смотрите такжеDon't just check errors, handle them gracefully, Не просто проверяйте ошибки, а корректно их обрабатывайте.

Не удалось обработать утверждение типа

type assertionЕдинственная форма возврата ' будет паниковать из-за неправильного типа. Поэтому всегда используйте идиому «запятая ок».

Bad Good
t := i.(string)
t, ok := i.(string)
if !ok {
  // 优雅地处理错误
}

не паникуй

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

Bad Good
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 не является стратегией обработки ошибок. Программа должна паниковать только тогда, когда происходит что-то неисправимое (например, нулевая ссылка). Инициализация программы является исключением: нежелательные условия, которые должны привести к прерыванию программы при ее запуске, могут вызвать панику.

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

Предпочитаю использовать даже в тестовом кодеt.Fatalилиt.FailNowвместо паники, чтобы убедиться, что сбой отмечен.

Bad Good
// 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")
}

Используйте go.uber.org/atomic

использоватьsync/atomicАтомарные операции над примитивными типами (int32, int64и т. д.), потому что легко забыть использовать атомарные операции для чтения или изменения переменных.

go.uber.org/atomicБезопасность типов добавляется к этим операциям путем сокрытия базового типа. Кроме того, он включает в себя удобныйatomic.Boolтип.

Bad Good
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()
}

представление

Конкретные рекомендации по производительности применяются только к высокочастотным сценариям.

Предпочитаю strconv, а не fmt

При преобразовании примитивов в строки или из строкstrconvсоотношение скоростейfmtбыстрый.

Bad Good
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

Избегайте преобразования строки в байт

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

Bad Good
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

Попробуйте указать емкость карты при инициализации

Насколько это возможно, используйтеmake()Предоставление информации о емкости во время инициализации

make(map[T1]T2, hint)

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

Bad Good
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создается без подсказок по размеру; во время выполнения может быть больше выделений.

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

Спецификация

последовательность

Некоторые из критериев, изложенных в этой статье, являются объективными оценками, основанными на контексте, контексте или субъективных суждениях;

Но самое главное,быть последовательным.

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

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

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

Похожие объявления группируются вместе

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

Bad Good
import "a"
import "b"
import (
  "a"
  "b"
)

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

Bad Good

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
)

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

Bad Good
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"

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

Bad Good
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.

Bad Good
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иРуководство по стилю Go Pack.

Имя функции

Мы следим за сообществом Go об использованииMixedCaps как имя функциисоглашение. За одним исключением, чтобы сгруппировать связанные тестовые примеры, имена функций могут содержать символы подчеркивания, например:TestMyFunction_WhatIsBeingTested.

импортировать псевдоним

Если имя пакета не соответствует последнему элементу пути импорта, необходимо использовать псевдоним импорта.

import (
  "net/http"

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

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

Bad Good
import (
  "fmt"
  "os"

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

  nettrace "golang.net/x/trace"
)

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

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

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

После определения типа, но перед остальными методами получателя,newXYZ()/NewXYZ()

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

Bad Good
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 {...}

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

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

Bad Good
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()
}

ненужное еще

Если переменная установлена ​​в обеих ветвях if, ее можно заменить одним if.

Bad Good
var a int
if b {
  a = 100
} else {
  a = 10
}
a := 10
if b {
  a = 100
}

объявление переменной верхнего уровня

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

Bad Good
var _s string = F()

func F() string { return "A" }
var _s = F()
// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
// 还是那种类型

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 类型

Для неэкспортируемых констант и переменных верхнего уровня используйте префикс _.

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

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

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

Bad Good
// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

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

  // We will not see a compile error if the first line of
  // Bar() is deleted.
}
// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

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

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

Bad Good
type Client struct {
  version int
  http.Client
}
type Client struct {
  http.Client

  version int
}

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

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

Bad Good
k := User{"John", "Doe", true}
k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

Исключение: Имена полей могут быть опущены в тестовой таблице, если есть 3 или меньше полей.

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

объявление локальной переменной

Если вы явно устанавливаете переменную в значение, вы должны использовать короткую форму объявления переменной (:=).

Bad Good
var s = "foo"
s := "foo"

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

Bad Good
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, что означает,

  • Вы не должны явно возвращать срез нулевой длины. должен вернутьсяnilвместо.

    Bad Good
    if x == "" {
      return []int{}
    }
    
    if x == "" {
      return nil
    }
    
  • Чтобы проверить, пуст ли срез, всегда используйтеlen(s) == 0. вместоnil.

    Bad Good
    func isEmpty(s []string) bool {
      return s == nil
    }
    
    func isEmpty(s []string) bool {
      return len(s) == 0
    }
    
  • Срез с нулевым значением (сvarобъявленные слайсы) доступны сразу без вызоваmake()Создайте.

    Bad Good
    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)
    }
    

малая переменная область видимости

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

Bad Good
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
 return err
}
if err := ioutil.WriteFile(name, data, 0644); err != nil {
 return err
}

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

Bad Good
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 (/* ... */)

Bad Good
// 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)

Используйте необработанные строковые литералы, избегайте экранирования

Go поддерживает использованиенеобработанный строковый литерал, то есть " ` " для представления собственной строки. В сценариях, которые необходимо экранировать, мы должны попытаться использовать эту схему для замены.

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

Bad Good
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

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

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

Bad Good
sval := T{Name: "foo"}

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

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

Инициализировать карты

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

Bad Good
var (
  // m1 读写安全;
  // m2 在写入时会 panic
  m1 = map[T1]T2{}
  m2 map[T1]T2
)
var (
  // m1 读写安全;
  // m2 在写入时会 panic
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

Декларация и инициализация выглядят очень похоже.

Декларация и инициализация выглядят очень по-разному.

Если возможно, укажите емкость карты во время инициализации.Подробнее см.Попробуйте указать емкость карты при инициализации.

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

Bad Good
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(Если возможно, попробуйте указать емкость карты).

строковый формат строки

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

это может помочьgo vetВыполните статический анализ строки формата.

Bad Good
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

Функции для именования стилей Printf

утверждениеPrintf-style, убедитесь, чтоgo vetЕго можно обнаружить и проверить строку формата.

Это означает, что вы должны использовать предопределенныйPrintfимя функции в стиле.go vetОни будут проверены по умолчанию. Для получения дополнительной информации см.Серия Printf.

Если предопределенное имя нельзя использовать, завершите выбранное имя буквой f:Wrapf, вместоWrap.go vetМожно попросить проверить определенное имя стиля Printf, но имя должно начинаться сfконец.

$ go vet -printfuncs=wrapf,statusf

смотрите такжеgo vet: Printf family check.

режим программирования

табличное тестирование

Когда логика теста повторяется, передатьsubtestsНаписание case-кода табличным способом выглядит намного чище.

Bad Good
// 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)
  })
}

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

Мы следуем соглашению, что мы называем структурные срезы какtests. Каждый тестовый пример называетсяtt. Кроме того, мы рекомендуем использоватьgiveиwantПрефиксы описывают входные и выходные значения для каждого теста.

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

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

Параметры функций

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

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

Bad Good
// 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),
)

Вы также можете обратиться к следующей информации:

Эта статья отредактирована или перепечатана учебными заметками zshipu.com.Если есть какие-либо нарушения, пожалуйста, свяжитесь с нами, и это должно быть изменено.