Основные рекомендации по стандартам кодирования на Golang
Используйте defer для освобождения ресурсов
Используйте defer для освобождения ресурсов, таких как файлы и блокировки.
Не рекомендуется:
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// Легко забыть разблокировать при наличии нескольких возвратов
Рекомендуется:
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// Более читаемо
Накладные расходы на defer крайне низкие, поэтому их следует избегать только тогда, когда можно доказать, что время выполнения функции находится на уровне наносекунд. Использование defer для улучшения читаемости стоит того, потому что стоимость их использования ничтожна. Это особенно применимо к большим методам, включающим не только простой доступ к памяти, где потребление ресурсов других вычислений намного превышает использование defer
.
Размер канала должен быть равен 1 или не буферизованным
Каналы обычно должны иметь размер 1 или быть не буферизованными. По умолчанию каналы являются не буферизованными с размером 0. Любой другой размер должен быть строго обсужден. Мы должны рассмотреть как определить размер, учитывая, что мешает каналу писать при высоких нагрузках и при блокировке, и какие изменения происходят в логике системы при этом.
Не рекомендуется:
// Должно быть достаточно для обработки любой ситуации!
c := make(chan int, 64)
Рекомендуется:
// Размер: 1
c := make(chan int, 1) // или
// Не буферизованный канал, размер 0
c := make(chan int)
Перечисления начинаются с 1
Стандартный способ введения перечислений в Go - объявлять пользовательский тип и группу const, использующую iota. Поскольку значение переменных по умолчанию равно 0, перечисления обычно должны начинаться с ненулевого значения.
Не рекомендуется:
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
Использование atomic
Используйте атомарные операции из пакета sync/atomic для операций с примитивными типами (int32
, int64
и т. д.), потому что легко забыть использовать атомарные операции для чтения или изменения переменных.
go.uber.org/atomic добавляет безопасность типов к этим операциям, скрывая базовый тип. Кроме того, он включает удобный тип atomic.Bool
.
Не рекомендуется:
type foo struct {
running int32 // atomic
}
func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// уже запущено…
return
}
// запустить Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // гонка!
}
Рекомендуется:
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// уже запущено…
return
}
// запустить Foo
}
func (f *foo) isRunning() bool {
return f.running.Load()
}
Избегайте изменяемых глобальных переменных
Используйте подход внедрения зависимостей, чтобы избежать изменения глобальных переменных. Это применимо как для указателей на функции, так и для других типов значений.
Не рекомендуемый подход 1:
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
Рекомендуемый подход 1:
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
Не рекомендуемый подход 2:
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
Рекомендуемый подход 2:
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
Избегайте использования предварительно объявленных идентификаторов
В спецификации языка Go описаны несколько предварительно объявленных идентификаторов, которые не следует использовать в проектах Go. Эти предварительно объявленные идентификаторы не должны быть повторно использованы в разных контекстах, поскольку это скроет исходные идентификаторы в текущей области видимости (или в любой вложенной области видимости), что может привести к путанице в коде. В лучшем случае компилятор выдаст ошибку, в худшем случае такой код может привести к потенциальным сложным ошибкам, из которых трудно восстановиться.
Нерекомендуемая практика 1:
var error string
// `error` неявно скрывает встроенный идентификатор
// или
func handleErrorMessage(error string) {
// `error` неявно скрывает встроенный идентификатор
}
Рекомендуемая практика 1:
var errorMessage string
// `error` теперь указывает на нескрытый встроенный идентификатор
// или
func handleErrorMessage(msg string) {
// `error` теперь указывает на нескрытый встроенный идентификатор
}
Нерекомендуемая практика 2:
type Foo struct {
// Хотя эти поля технически не скрывают, повторное определение строк `error` или `string` становится теперь неоднозначным.
error error
string string
}
func (f Foo) Error() error {
// `error` и `f.error` визуально выглядят похоже
return f.error
}
func (f Foo) String() string {
// `string` и `f.string` визуально выглядят похоже
return f.string
}
Рекомендуемая практика 2:
type Foo struct {
// `error` и `string` теперь явны.
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
Обратите внимание, что компилятор не будет выдавать ошибок при использовании предварительно объявленных идентификаторов, однако такие инструменты, как go vet
, правильно указывают на эти и другие связанные неявно проблемы.
Избегайте использования init()
Постарайтесь избегать использования init()
насколько это возможно. Когда использование init()
неизбежно или предпочтительно, код должен:
- Обеспечить полноту независимо от среды программы или вызова.
- Избегать полагаться на порядок или побочные эффекты других функций
init()
. Хотя порядокinit()
является явным, код может измениться, поэтому отношение между функциямиinit()
может сделать код хрупким и ошибочным. - Избегать доступа к глобальным или окружающим состояниям, таким как информация о машине, переменные среды, рабочие каталоги, параметры/входные данные программы и т. д.
- Избегать ввода-вывода, включая файловые системы, сетевые операции и системные вызовы.
Код, не соответствующий этим требованиям, может быть частью вызова main()
(или другого места в жизненном цикле программы) или быть написанным как часть main()
самого по себе. В частности, библиотеки, предназначенные для использования другими программами, должны обращать особое внимание на полноту, а не на выполнение "волшебства инициализации".
Непринятый подход 1:
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
Рекомендуемый подход 1:
var _defaultFoo = Foo{
// ...
}
// или, для лучшей тестируемости:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
Непринятый подход 2:
type Config struct {
// ...
}
var _config Config
func init() {
// Неправильно: на основе текущего каталога
cwd, _ := os.Getwd()
// Неправильно: ввод-вывод
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
Рекомендуемый подход 2:
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// обработать ошибку
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// обработать ошибку
var config Config
yaml.Unmarshal(raw, &config)
return config
}
Учитывая вышеперечисленные соображения, в некоторых случаях использование init()
может быть более предпочтительным или необходимым, включая:
- Не может быть представлено как одиночное присвоение сложного выражения.
- Вставляемые хуки, такие как
database/sql
, реестры типов и т. д.
Предпочтительно указывать вместимость среза при добавлении
Всегда отдавайте приоритет указанию значения вместимости для make()
, при инициализации среза для добавления.
Непринятый подход:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Рекомендуемый подход:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Использование тегов полей при сериализации структур
При сериализации в JSON, YAML или любой другой формат, который поддерживает именование полей на основе тегов, следует использовать соответствующие теги для аннотации.
Не рекомендуется:
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
Рекомендуется:
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Безопасно переименовать Name в Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
В теории, формат сериализации структуры является контрактом между различными системами. Внесение изменений в формат сериализации структуры (включая имена полей) нарушит этот контракт. Указание имен полей в тегах делает контракт явным и также помогает предотвратить случайные нарушения контракта при рефакторинге или переименовании полей.