Для более полного и глубокого понимания рекомендуется читать параллельно с английской версией.
- Методические рекомендации по написанию Go кода от Uber
- Дисклеймер
- Содержание
- Введение
- Методические указания
- Указатели на интерфейсы
- Получатели и интерфейсы
- Zero-value Mutexes are Valid
- Копирование срезов и мапов на границах
- Используйте Defer для освобождения ресурсов
- Channel Size is One or None
- Начинайте перечисления (Enums) с единицы
- Типизация ошибок
- Error Wrapping
- Handle Type Assertion Failures
- Не паниковать
- Use go.uber.org/atomic
- Производительность
- Style
- Be Consistent
- Группируйте похожие объявления
- Порядок импорта пакетов
- Названия пакетов
- Названия функций
- Псевдонимы импортов
- Группировка и упорядочивание функций
- Уменьшение вложенности
- Излишние Else
- Объявление верхнеуровневых переменных
- Используйте префикс _ для глобальных неэкспортируемых переменных
- Встраивание в структуры
- Используйте названия полей при инициализации структур
- Определение локальных переменных
- nil это полноценный срез
- Уменьшайте область видимости переменных
- Избегайте прямых аргументов
- Use Raw String Literals to Avoid Escaping
- Инициализация ссылок на структуры
- Инициализация мап
- Строки форматирования за Printf
- Naming Printf-style Functions
- Паттерны
Стили - это соглашения, определяющие качество нашего кода. Термин стиль является не слишком полным, так как данное соглашение описывает гораздо больше, чем просто форматирование исходного кода программы, c которым и так прекрасно справляется gofmt.
Целью данного руководства является упрощение понимания, того как как можно и нужно, а как нельзя писать код на Go в Uber. Эти правила необходимы для того, чтобы сохранить контроль над кодовой базой проекта и при этом позволить программистам эффективно использовать возможности языка Go.
Данное руководство было создано [Прашантом Варанаси] и [Саймоном Ньютоном] как способ помочь коллегам начать использовать Go. С течением времени в него были внесены изменения на основе обратной связи от читателей.
Данный документ является соглашением, которому мы следуем в Uber. Многое из этого является общими рекомендациями для написания кода на Go, в то время как некоторые вещи были почерпнуты из внешних источников:
Код не должен содержать ошибок при запуске golint
и go vet
. Мы рекомендуем настроить ваш редактор на:
- Запуск
goimports
во время сохранения - Запуск
golint
иgo vet
для проверки на ошибки
Информацию по поддержке Go инструментов вашим редактором можно найти здесь: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
Вам практически никогда не понадобится указатель на интерфейс. Интерфейсы необходимо передавать по значению, в то время как данные интерфейсов могут содержать в себе указатель.
Интерфейс содержит в себе два поля:
- Указатель на type-specific информацию. Можете принять это поле как "тип".
- Указатель на данные. Если поле содержит указатель, то он сохраняется напрямую. Если поле содержит значение, то сохраняется указатель на это значение.
Если вы хотите, чтобы интерфейс мог изменять данные, то вам необходимо использовать указатель.
Методы с получателями по значению могут также вызываться указателями.
Например,
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.
The zero-value of sync.Mutex
and sync.RWMutex
is valid, so you almost
never need a pointer to a mutex.
Bad | Good |
---|---|
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 для освобождения ресурсов, таких как файлы и блокировки.
Плохо | Хорошо |
---|---|
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
.
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.
Bad | Good |
---|---|
// 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) |
Стандартный путь объявления перечислений в 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")
}
}
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:
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)
} |
|
|
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.
The single return value form of a type assertion will panic on an incorrect type. Therefore, always use the "comma ok" idiom.
Bad | Good |
---|---|
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.
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")
} |
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.
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()
} |
Performance-specific guidelines apply only to the hot path.
При конвертации типов в/из строки, 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())
} |
|
|
Не приводите строку в слайс байтов много раз. Вместо этого выполните преобразование один раз и сохраните результат.
Плохо | Хорошо |
---|---|
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)
} |
|
|
Где возможно, старайтесь объявлять значение 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
} |
|
|
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()
} |
Если переменной присваивается значение в обоих ветвях 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
является полноценный срезом длины 0. Это означает, что,
-
Не следует возвращать срез длины 0 явным образом. Вместо этого необходимо возвращать
nil
.Bad Good 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)
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.
Bad | Good |
---|---|
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
-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) |
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.
Use table-driven tests with subtests to avoid duplicating code when the core test logic is repetitive.
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)
})
} |
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) это паттерн, в котором вы определяете интерфейсный
тип 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),
) |
Смотрите также,