Посібник зі стилю Go від Uber source
#Вступ
Стилі — це домовленості щодо керування кодом. Термін “стиль” тут дещо вводить в оману,
оскільки описані тут угоди охоплюють набагато більше, ніж просто форматування вихідного файлу,
з яким і так чудово справляється gofmt
.
Мета цього посібника — структурувати дані домовленості шляхом детального опису того, як потрібно, а також як не потрібно писати код на Go в Uber. Ці правила існують для того, щоб зберегти кодову базу керованою і при цьому дозволити інженерам продуктивно використовувати можливості мови Go.
Даний посібник був створений Prashant Varanasi та Simon Newton, щоб ознайомити колег із використанням мови Go. Протягом багатьох років він змінювався та вдосконалювався на основі отриманих відгуків.
Ця документація містить багаті на ідіоми правила коду Go, яких дотримуються в Uber. Багато з них є загальними рекомендаціями для Go, в той час, як інші походять із зовнішніх джерел:
Ми прагнемо, щоб приклади коду були точними для двох останніх проміжних версій Go.
Під час запуску через golint
та go vet
, ваш код не повинен містити помилок.
Рекомендуємо налаштувати ваш редактор наступним чином:
- Запускати
goimports
під час збереження - Запускати
golint
таgo vet
для перевірки на наявність помилок
Інформацію про підтримку вашим редактором Go інструментів ви можете знайти тут: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
#Настанови
#Вказівники на інтерфейси
Вам майже ніколи не знадобиться вказівник на інтерфейс. Ви повинні передавати інтерфейси за значенням, оскільки дані, що лежать в основі, завжди можуть бути вказівником.
Інтерфейс складається з двох полів:
- Вказівник на певну інформацію про тип. Він представлений як “тип”.
- Вказівник на дані. Якщо дані містять вказівник, вони зберігаються напряму. Якщо дані містять значення, то зберігається вказівник на це значення.
Якщо ви хочете, щоб методи інтерфейсу могли б змінювати базові дані, то вам слід використовувати вказівник.
#Перевірка відповідності інтерфейсу
Якщо необхідно, перевірте відповідність інтерфейсу під час компіляції. Це включає:
- Експортовані типи, які необхідні для реалізації певних інтерфейсів згідно з контрактом API
- Експортовані або не експортовані типи, які є частиною групи типів, що реалізують той самий інтерфейс
- Інші випадки, коли недотримання інтерфейсу може спричинити проблеми для користувачів
Не рекомендовано | Рекомендовано |
---|---|
|
|
Оператор var _ http.Handler = (*Handler)(nil)
не вдасться скомпілювати, якщо
*Handler
перестане відповідати інтерфейсу http.Handler
.
Права сторона призначення має бути нульовим значенням (zero-value) заявленого типу.
Це nil
для вказівників (як *Handler
), зрізів (slices) і карт (maps), а також
порожня структура для типів структур.
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
#Одержувачі (receivers) та інтерфейси
Методи з одержувачами за значенням, можуть бути викликані як за вказівниками, так і за значеннями. Методи з одержувачами вказівника, можуть бути викликані лише через вказівник або адресовані значення.
Наприклад,
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 є значенням, а для f немає одержувача за значенням.
// i = s2Val
Effective Go чудово описує вказівники або значення.
#Дозволене використання м’ютексів (mutex) з нульовими значеннями
Нульові значення (zero-value) sync.Mutex
та sync.RWMutex
є правильними, тому вам майже ніколи
не потрібно використовувати вказівник на м’ютекс.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Якщо ви використовуєте структуру за вказівником, тоді м’ютекс має бути полем без вказівника. Не вставляйте м’ютекс у структуру, навіть якщо структуру не було експортовано.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Поле |
М’ютекс та його методи є деталями реалізації |
#Обмеження копіювання зрізів (slices) та карт (maps)
Зрізи та карти містять вказівники на основні дані, тому будьте обережні зі сценаріями, коли їх потрібно скопіювати.
#Отримання зрізів і карт
Майте на увазі, що користувачі можуть змінювати карту або зріз, які ви отримали як аргумент, якщо ви зберігаєте посилання на них.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Повернення зрізів і карт
Подібним чином, будьте обережні з модифікаціями користувачами карт або зрізів, що розкривають внутрішній стан.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Defer для звільнення ресурсів
Використовуйте defer
для звільнення ресурсів, таких як файли та блокування (locks).
Не рекомендовано | Рекомендовано |
---|---|
|
|
Defer має надзвичайно низькі витрати ресурсів і тому його слід уникати в тих випадках, якщо ви можете довести,
що час виконання вашої функції становить наносекунди. Перевага оператора defer
щодо зручності
читання вашого коду вартує тих мізерних витрат ресурсів на його використання.
Це особливо має відношення до більших методів, які мають більше, ніж простий доступ до пам’яті,
де інші обчислення важливіші ніж defer
.
#Розмір каналу (channel) дорівнює одиниці або не вказано
Канали (channels) зазвичай повинні мати розмір, який дорівнює одиниці або ж бути небуферизованими. За замовчуванням, канали не буферизовані та мають нульовий розмір. Будь-який інший розмір повинен ретельно контролюватися. Розглянемо, як визначається розмір, який заважає каналу заповнюватися під навантаженням та блокує запис, і що відбувається, коли такий сценарій стався.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Починайте перерахування (enums) з одиниці
Стандартним способом впровадження enums у Go є оголошення власного типу
та групи const
за допомогою iota
. Оскільки змінні за замовчуванням мають значення, яке дорівнює 0,
ви зазвичай повинні починати свої enums з ненульових значень, наприклад з 1.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Бувають випадки, коли використання нульового значення має сенс, наприклад, в ситуації, коли нульове значення є бажаною поведінкою за замовчуванням.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
#Використовуйте пакет "time"
для обробки часу
Управління часом є складною темою. Неправильні припущення, які часто пов’язанні з “часом”, припускають наступне.
- Тривалість доби становить 24 години
- Година має 60 хвилин
- Тиждень має 7 днів
- Рік має 365 днів
- Та багато іншого
Наприклад, 1 означає, що додавання 24 годин до певного моменту часу не гарантує, що ви отримаєте інший календарний день.
Тому завжди використовуйте пакет "time"
коли маєте справу з часом, оскільки він
допомагає впоратися з цими неправильними припущеннями безпечнішим та значно точнішим способом.
#Використовуйте time.Time
для моментів з часом
Використовуйте time.Time
коли маєте справу з моментами часу, а також методи time.Time
для
порівняння, додавання або віднімання часу.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Використовуйте time.Duration
для проміжків часу
Використовуйте time.Duration
коли маєте справу з часовими проміжками.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Повертаючись до прикладу з додаванням 24 годин до певного моменту часу,
метод, який ми використовуємо для додавання часу, залежить від наших намірів.
Якщо ми хочемо отримати той самий час доби, але наступного календарного дня, ми повинні
використовувати Time.AddDate
. Однак, якщо ми хочемо гарантувати, що отримаємо момент часу,
зміщений на 24 години після попереднього часу, то слід використовувати Time.Add
.
newDay := t.AddDate(0 /* роки */, 0 /* місяці */, 1 /* дні */)
maybeNewDay := t.Add(24 * time.Hour)
#Використовуйте time.Time
та time.Duration
із зовнішніми системами
Використовуйте time.Duration
та time.Time
у взаємодії із зовнішніми системами, коли це можливо.
Наприклад:
-
Прапори командного рядка:
flag
підтримуютьtime.Duration
черезtime.ParseDuration
-
JSON:
encoding/json
підтримує кодуванняtime.Time
як RFC 3339 рядок через йогоUnmarshalJSON
метод -
SQL:
database/sql
підтримує перетворенняDATETIME
абоTIMESTAMP
стовпців вtime.Time
і назад, якщо це підтримує базовий драйвер -
YAML:
gopkg.in/yaml.v2
підтримуєtime.Time
як RFC 3339 рядок, таtime.Duration
черезtime.ParseDuration
.
Якщо неможливо використати time.Duration
у цих взаємодіях, використовуйте
int
або float64
та включіть одиницю часу в назву поля.
Наприклад, оскільки encoding/json
не підтримує time.Duration
, одиниця часу включається в назву поля.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Якщо неможливо використовувати time.Time
у цих взаємодіях та якщо інше не погоджено,
використовуйте string
і форматуйте мітки часу (timestamps), як визначено в RFC 3339.
Цей формат використовується за замовчуванням Time.UnmarshalText
та доступний
для використання в Time.Format
і time.Parse
через time.RFC3339
.
Хоча на практиці це не є проблемою, майте на увазі, що пакет "time"
не підтримує
розбір позначок часу (timestamps) з високосними (додатковими) секундами (8728),
а також не враховує високосні секунди в обчисленнях (15190). Якщо ви порівнюєте два моменти часу,
різниця не включатиме високосні секунди, які могли відбутися між цими двома моментами.
#Помилки
#Типи помилок
Існує кілька варіантів оголошення помилок. Перш ніж вибрати варіант, який найкраще підходить для вашого випадку, врахуйте наступне.
- Чи потрібно клієнту зіставити помилку з іншим типом помилки, щоб обробити її?
Якщо так, нам потрібно підтримувати функції
errors.Is
абоerrors.As
, оголошуючи змінну помилки вищого рівня або власного (кастомного) типу. - Повідомлення про помилку це статичний рядок чи динамічний, для якого потрібна
контекстна інформація?
Для першого ми можемо використовувати
errors.New
, але для останнього ми повинні використовуватиfmt.Errorf
або власний тип помилки. - Передаєте помилку з функцій, яка розташована нижче по стеку викликів? Тоді перегляньте [розділ про упакування помилок](#упакування помилок-wrapping).
Зіставлення помилки? | Повідомлення про помилку | Рекомендація |
---|---|---|
ні | статична | errors.New |
ні | динамічна | fmt.Errorf |
так | статична | верхнього рівня var з errors.New |
так | динамічна | власний тип error |
Наприклад, використовуйте errors.New
для помилки зі статичним рядком.
Якщо клієнту потрібно знайти відповідність і обробити помилку, експортуйте цю помилку
як змінну, щоб була підтримка зіставлення з errors.Is
.
Немає зіставлення помилки | Зіставлення помилки |
---|---|
|
|
Для помилки з динамічним рядком, використовуйте fmt.Errorf
якщо клієнту не потрібна
перевірка зіставлення або власний error
, якщо клієнт потребує перевірки зіставлення з помилкою.
Немає зіставлення помилки | Зіставлення помилки |
---|---|
|
|
Врахуйте, якщо ви експортуєте змінні або типи помилок із пакета, вони стануть частиною загальнодоступного API пакета.
#Упакування помилок (wrapping)
Є три основні способи поширення помилок у разі невдачі під час виклику:
- повернути оригінальну помилку “як є”
- додати контекст за допомогою
fmt.Errorf
та параметру%w
- додати контекст за допомогою
fmt.Errorf
та параметру%v
Повернути оригінальну помилку “як є”, якщо вам не потрібно додавати додаткову контекстну інформацію. Це зберігає вихідний тип помилки та повідомлення. Це добре підходить для випадків, коли базове повідомлення про помилку містить достатньо інформації, щоб визначити, звідки вона походить.
В іншому випадку додайте контекст до повідомлення про помилку, де це можливо, щоб замість не зрозумілої помилки, як-от “підключення відмовлено”, ви отримували більш корисні помилки, наприклад “виклик служби foo: підключення відмовлено”.
Використовуйте fmt.Errorf
, щоб додати контекст до ваших помилок, вибираючи між параметрами
%w
або %v
залежно від того, чи повинен клієнт мати можливість знайти та виділити основну
причину.
- Використовуйте
%w
, якщо клієнт повинен мати доступ до основної помилки. Це хороший варіант за замовчуванням для більшості обгорнутих (wrapped) помилок, але майте на увазі, що клієнти можуть почати покладатися на таку поведінку. Тож у випадках, коли загорнута помилка є відомоюvar
або типом, задокументуйте та протестуйте її як частину контракту вашої функції. - Використовуйте
%v
, щоб приховати основну помилку. Клієнти не зможуть порівнювати з оригінальною помилкою, але ви можете перемикнутися на%w
в майбутньому, якщо буде потрібно.
Додаючи контекст до помилок, зберігайте його лаконічним, уникаючи таких фраз, як “не вдалося” (“failed to”), які стверджують очевидне та накопичуються, коли помилка просочується крізь стек:
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
Однак після надсилання повідомлення про помилку в іншу систему має бути зрозуміло,
що повідомлення є помилкою (наприклад, тег err
або префікс “Failed” в журналах).
Дивіться також Не просто перевіряйте помилки, обробляйте їх витончено.
#Іменування помилок
Для значень помилок, які зберігаються як глобальні змінні,
використовуйте префікс Err
або err
залежно від того, експортовані вони чи ні.
Ці рекомендації замінюють Використовуйте префікс _
для не експортованих глобальних змінних.
var (
// Наступні дві помилки експортовано, щоб
// користувачі даного пакету могли порівняти їх з errors.Is.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// Ця помилка не експортується, оскільки ми не хочемо
// робити її частиною нашого загальнодоступного API.
// Ми все ще можемо використовувати її в середині пакету з errors.Is.
errNotFound = errors.New("not found")
)
Для власних типів помилок використовуйте суфікс Error
.
// Подібним чином ця помилка експортується, щоб
// користувачі даного пакету могли зіставити її з errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// Ця помилка не експортується, тому що ми не хочемо
// робити її частиною публічного API.
// Ми все ще можемо використовувати її в середині пакету з errors.As.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
#Обробка помилок підтвердження типу
Форма підтвердження типу з єдиним значенням, що повертається, викличе паніку через неправильний тип. Тому завжди використовуйте ідіому “кома добре” (“comma ok”).
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Уникайте паніки
Код, який працює у виробничому середовищі (production), повинен уникати паніки. Паніка є основним джерелом каскадних збоїв. Якщо виникає помилка, функція повинна повернути помилку та дозволити користувачу вирішити, як її обробити.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Panic/recover не є стратегією обробки помилок. Програма повинна панікувати лише тоді, коли трапляється щось непоправне, наприклад, nil розіменування (nil dereference). Винятком є ініціалізація програми: помилки під час запуску програми, які мають порушити роботу програми, можуть викликати паніку.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Навіть у тестах віддавайте перевагу t.Fatal
або t.FailNow
замість паніки,
щоб гарантувати, що тест буде позначено як невдалий.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Використовуйте go.uber.org/atomic
Атомарні операції з пакетом sync/atomic працюють із необробленими типами
(int32
, int64
тощо), тому легко забути використовувати атомарну операцію
для читання або модифікації змінних.
go.uber.org/atomic додає цим операціям захист типу, приховуючи базовий тип.
Крім того, він містить зручний тип atomic.Bool
.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Уникайте непостійних (mutable) глобальних змінних
Уникайте зміни глобальних змінних, натомість вибирайте впровадження залежностей (dependency injection). Це стосується вказівників на функції, а також інших типів значень.
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
#Уникайте вбудовування типів (type embedding) у публічні структури
Типи, вбудовані в публічні структури, пропускають деталі реалізації, обмежують еволюцію типів і негативно впливають на якість документації.
Якщо припустити, що ви реалізували різні типи списків за допомогою спільного
AbstractList
, уникайте вбудовування AbstractList
у ваші конкретні реалізації списків.
Натомість додайте до свого конкретного списку методи, які будуть делегувати
завдання методам абстрактного списку AbstractList
.
type AbstractList struct {}
// Add додає сутність до списку.
func (l *AbstractList) Add(e Entity) {
// ...
}
// Remove видаляє сутність зі списку.
func (l *AbstractList) Remove(e Entity) {
// ...
}
Не рекомендовано | Рекомендовано |
---|---|
|
|
Go дозволяє вбудовування типу як компроміс між наслідуванням та композицією. Зовнішній тип отримує неявні копії методів вбудованого типу. За замовчуванням ці методи делегують завдання методам вбудованого екземпляра.
Структура також отримує поле з тим же іменем, що й тип. Отже, якщо вбудований тип загальнодоступний, поле також буде публічним. Для зворотної сумісності, будь-яка майбутня версія зовнішнього типу повинна зберігати вбудований тип.
Вбудований тип рідко буває необхідним. В основному це зручний спосіб уникнути виснажливого написання методів делегування.
Навіть вбудовування сумісного інтерфейсу AbstractList
замість структури
дасть розробнику більше гнучкості для внесення змін в майбутньому,
але все одно призведе до витоку інформації про те, що конкретні списки
використовують абстрактну реалізацію.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Чи це вбудована структура, чи вбудований інтерфейс, вбудований тип обмежує еволюцію типів.
- Додавання методів до вбудованого інтерфейсу порушує сумісність (breaking changes).
- Видалення методів із вбудованої структури порушує сумісність.
- Видалення вбудованого типу порушує сумісність.
- Заміна вбудованого типу, навіть якщо заміна відповідає тому самому інтерфейсу, порушує сумісність.
Попри те, що написання цих методів делегування (методи, визначені в інтерфейсі) є громіздким, додаткові зусилля приховують деталі реалізації, залишають більше можливостей для змін, а також усувають непрямий доступ до повного інтерфейсу List в документації.
#Уникайте використання вбудованих імен
У специфікації мови Go описано декілька вбудованих попередньо визначених ідентифікаторів, які не слід використовувати як імена в програмах Go.
Залежно від контексту повторне використання цих ідентифікаторів як імен або приховає оригінал у межах поточної лексичної області (і всіх вкладених областях), або призведе до заплутування коду. У кращому випадку можна очікувати попереджень від компілятора, у гіршому випадку такий код може створити приховані помилки, які важко помітити.
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
Зауважте, що компілятор не генеруватиме помилок під час використання попередньо оголошених
ідентифікаторів, але такі інструменти, як go vet
, мають правильно вказувати на ці та інші
випадки приховування вбудованих імен.
#Уникайте init()
Уникайте init()
де це можливо. Якщо використання init()
неминуче або ж бажане,
ваш код повинен:
- Бути повністю детермінованим, незалежно від програмного середовища чи виклику.
- Уникайте залежності від порядку або побічних ефектів інших функцій
init()
. Хоча порядок викликуinit()
добре відомий, код може змінюватися, і відповідно зв’язки між функціямиinit()
можуть зробити код крихким та схильним до помилок. - Уникайте доступу або маніпулювання глобальним станом або станом середовища, таким як інформація про машину, змінні середовища, робочий каталог, аргументи виклику, вхідні дані програми тощо.
- Уникайте операцій вводу-виводу (I/O), включаючи файлову систему, мережу та системні виклики.
Код, який не відповідає наведеним вище вимогам, швидше за все, буде допоміжним кодом,
який буде викликатися як частина main()
(або в іншому місці життєвого циклу програми),
або буде написаний як частина самого main()
.
Зокрема, бібліотеки, що призначені для використання іншими програмами, повинні бути
особливо обережними, щоб бути повністю детермінованими та не виконувати “магію ініціалізації”.
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
Враховуючи вищезазначене, деякі ситуації, в яких init()
може бути кращим або необхідним,
можуть включати:
-
Складні вирази, які не можна представити як окреме призначення.
-
Підключаються хуки, такі як діалекти
database/sql
, реєстри типів кодування тощо. -
Оптимізація для Google Cloud Functions та інших форм детермінованого попереднього обчислення.
#Вихід в Main
Програми Go використовують os.Exit
або log.Fatal*
для негайного виходу.
(Паніка не є хорошим способом виходу з програм, будь ласка, не панікуйте.)
Викликайте os.Exit
або log.Fatal*
лише в main()
.
Усі інші функції повинні повертати помилки, щоб повідомляти про збій.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Обґрунтування: програми з кількома функціями, які містять вихід (exit), викликають кілька проблем:
- Неочевидний потік керування: будь-яка функція може вийти з програми, тому стає важко міркувати про потік керування.
- Важко тестувати: функція, яка виходить з програми, також вийде із тесту, який її викликає.
Це ускладнює тестування функції та створює ризик пропуску інших тестів,
які ще не були запущені
go test
. - Пропущене звільнення ресурсів: коли функція виходить з програми, вона пропускає виклики функцій,
поставлених у чергу з операторами
defer
. Це збільшує ризик пропуску важливих завдань очищення (звільнення ресурсів).
#Виходьте один раз
Якщо можливо, надайте перевагу виклику os.Exit
або log.Fatal
не більше одного разу
у вашій функції main()
. Якщо існує кілька сценаріїв помилок, які зупиняють виконання програми,
помістіть цю логіку в окрему функцію та вже з неї повертайте помилки.
Це призводить до скорочення вашої функції main()
та тримає всю ключову бізнес-логіку
в окремій функції, яку можна протестувати.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Використовуйте теги полів у серіалізованих структурах
Будь-яке поле структури, серіалізоване в JSON, YAML або інші формати, які підтримують іменування полів на основі тегів, має бути анотовано відповідним тегом.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Обґрунтування: Серіалізована форма структури є контрактом між різними системами. Зміни в структурі серіалізованої форми, включаючи імена полів, порушують цей контракт. Додання імен полів всередині тегів, дозволяє зробити контракт явним та захищеним від випадкового порушення контракту шляхом рефакторингу або перейменування полів.
#Don’t fire-and-forget goroutines
Goroutines are lightweight, but they’re not free: at minimum, they cost memory for their stack and CPU to be scheduled. While these costs are small for typical uses of goroutines, they can cause significant performance issues when spawned in large numbers without controlled lifetimes. Goroutines with unmanaged lifetimes can also cause other issues like preventing unused objects from being garbage collected and holding onto resources that are otherwise no longer used.
Therefore, do not leak goroutines in production code. Use go.uber.org/goleak to test for goroutine leaks inside packages that may spawn goroutines.
In general, every goroutine:
- must have a predictable time at which it will stop running; or
- there must be a way to signal to the goroutine that it should stop
In both cases, there must be a way code to block and wait for the goroutine to finish.
For example:
Bad | Good |
---|---|
|
|
There’s no way to stop this goroutine. This will run until the application exits. |
This goroutine can be stopped with |
#Wait for goroutines to exit
Given a goroutine spawned by the system, there must be a way to wait for the goroutine to exit. There are two popular ways to do this:
-
Use a
sync.WaitGroup
. Do this if there are multiple goroutines that you want to wait forvar wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) go func() { defer wg.Done() // ... }() } // To wait for all to finish: wg.Wait()
-
Add another
chan struct{}
that the goroutine closes when it’s done. Do this if there’s only one goroutine.done := make(chan struct{}) go func() { defer close(done) // ... }() // To wait for the goroutine to finish: <-done
#No goroutines in init()
init()
functions should not spawn goroutines.
See also Avoid init().
If a package has need of a background goroutine,
it must expose an object that is responsible for managing a goroutine’s
lifetime.
The object must provide a method (Close
, Stop
, Shutdown
, etc)
that signals the background goroutine to stop, and waits for it to exit.
Bad | Good |
---|---|
|
|
Spawns a background goroutine unconditionally when the user exports this package. The user has no control over the goroutine or a means of stopping it. |
Spawns the worker only if the user requests it. Provides a means of shutting down the worker so that the user can free up resources used by the worker. Note that you should use |
#Продуктивність
Інструкції щодо продуктивності застосовуються лише до так званого “hot path” (шляхи виконання коду, де витрачається більша частина часу виконання і які можуть виконуватися дуже часто).
#Надавайте перевагу strconv
замість fmt
При конвертації типів в рядки/з рядків strconv
швидше, ніж fmt
.
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
#Уникайте конвертації string-to-byte
Не створюйте зріз байтів із фіксованого рядка декілька разів. Натомість виконайте конвертування один раз і збережіть результат.
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
#Намагайтесь вказувати місткість (capacity) контейнера
Де це можливо, вказуйте місткість контейнера, щоб наперед виділити відповідний обсяг пам’яті. Це мінімізує подальші виділення пам’яті (allocations) у міру додавання нових елементів (шляхом копіювання та зміни розміру контейнера).
#Вказуйте місткість для карт
Якщо це можливо, вкажіть місткість під час ініціалізації карт за допомогою make()
.
make(map[T1]T2, hint)
Задання місткості під час виклику make()
намагається підібрати правильний розмір карти
під час її ініціалізації, що зменшує кількість операцій виділення пам’яті під час
додання нових елементів до карти.
Пам’ятайте, що, на відміну від зрізів, місткість для карт не гарантує повного та остаточного виділення пам’яті, а використовуються для приблизної кількості необхідних сегментів хеш-карти. Отже, виділення додаткової пам’яті все одно можуть відбуватися під час додавання елементів до карти, навіть до вказаної наперед місткості.
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
#Вказуйте місткість для зрізів
Якщо це можливо, вказуйте місткість зрізів під час ініціалізації за допомогою make()
,
особливо при додаванні.
make([]T, length, capacity)
На відміну від карт, для місткості зрізів компілятор виділить достатньо
пам’яті, скільки було вказано в make()
. Це означає, що всі наступні операції append()
не вимагатимуть виділення додаткової пам’яті (допоки довжина зрізу відповідає місткості,
після чого будь-які додавання вимагатимуть зміни розміру, для додавання нових елементів).
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
#Стиль
#Уникайте надто довгих рядків
Уникайте рядків коду, які вимагають від читачів горизонтальної прокрутки або надто сильного повороту голови.
Ми рекомендуємо обмежити довжину м’якого рядка (soft line) до 99 символів. Автори повинні прагнути до того, щоб не виходити за рамки цих обмежень, хоча ці обмеження не є жорсткими. Код може перевищувати цю межу.
#Будьте послідовними
Деякі з настанов, викладених у цьому документі, можна оцінити об’єктивно; інші є ситуативними, контекстними або суб’єктивними.
Перш за все, будьте послідовними.
Послідовний код легше підтримувати та раціоналізувати, він потребує менше когнітивних витрат і його легше переносити чи оновлювати, коли з’являються нові угоди або виправляються класи помилок.
І навпаки, наявність кількох різнорідних або потенційно конфліктних стилів в одній кодовій базі спричиняє накладні витрати на технічне обслуговування, невизначеність і когнітивний дисонанс. Усе це може безпосередньо сприяти зниженню швидкості, болісним перевіркам коду та помилкам.
Застосовуючи ці вказівки до кодової бази, рекомендується вносити зміни на рівні пакета (або більше): застосування на рівні під-пакету порушує вищезазначені проблеми, додаючи кілька стилів до одного коду.
#Групуйте схожі оголошення
Go підтримує групування схожих декларацій.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Це також стосується констант, змінних і оголошень типів.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Групуйте лише пов’язані оголошення. Не групуйте декларації, які не мають нічого спільного.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Групи не мають обмежень щодо місця використання. Наприклад, ви можете використовувати їх всередині функцій.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Виняток: оголошення змінних, особливо всередині функцій, повинні бути згруповані разом, якщо вони оголошені поруч з іншими змінними. Зробіть це для змінних, оголошених разом, навіть якщо вони не мають нічого спільного.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Порядок імпортування бібліотек
Повинно бути дві групи імпорту:
- Стандартна бібліотека
- Все інше
Це групування, застосоване goimports
за замовчуванням.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Назви пакетів
При іменуванні пакетів, оберіть таку назву:
- Все з малих літер. Без великих літер і підкреслень.
- Не потрібно перейменовувати за допомогою іменованого імпорту на більшості викликів сайтів.
- Коротко і лаконічно. Пам’ятайте, що ім’я вказується повністю на кожному виклику сайту.
- Не множини. Наприклад,
net/url
, а неnet/urls
. - Не “common”, “util”, “shared” або “lib”. Це погані, не інформативні назви.
Дивіться також Package Names та Style guideline for Go packages.
#Назви функцій
Ми дотримуємося конвенцій спільноти Go щодо використання MixedCaps для імен функцій.
Виняток зроблено для тестових функцій, які можуть містити підкреслення з метою групування
пов’язаних тестів, наприклад, TestMyFunction_WhatIsBeingTested
.
#Імпорт псевдонімів
Псевдонім імпорту слід використовувати, якщо назва пакета не збігається з останнім елементом шляху імпорту.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
У всіх інших сценаріях слід уникати псевдонімів імпорту, якщо немає прямого конфлікту між імпортами.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Групування та впорядкування функцій
- Функції мають бути відсортовані в приблизному порядку викликів.
- Функції у файлі повинні бути згруповані відповідно до отримувача.
Таким чином, експортовані функції повинні з’явитися у файлі першими, одразу після визначень
struct
, const
та var
.
Функції newXYZ()
/NewXYZ()
можуть з’явитися після визначення типу,
але перед рештою методів одержувача.
Оскільки функції згруповані за одержувачами, звичайні службові функції мають з’являтися в кінці файлу.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Зменште вкладеність
Потрібно зменшувати вкладеність де це можливо, спочатку обробляючи випадки помилок або спеціальні умови та повертати результат раніше або продовжувати цикл. Зменште кількість коду вкладеного на кілька рівнів.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Зайвий оператор else
Якщо змінна встановлена в обох частинах if, її можна замінити одним if.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Оголошення змінних верхнього рівня
На верхньому рівні (рівні файлу) використовуйте стандартне ключове слово var
.
Не вказуйте тип, за винятком випадків, коли вираз не збігається з типом.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Вкажіть тип, якщо тип виразу не відповідає бажаному типу.
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F повертає об'єкт типу myError, але ми хочемо повернути error.
#Використовуйте префікс _
для не експортованих глобальних змінних
До не експортованих змінних var
та констант const
верхнього рівня додайте префікс _
,
щоб під час їх використання було зрозуміло, що вони є глобальними символами.
Пояснення: змінні та константи верхнього рівня мають область видимості всього пакету. Використання загальних імен дозволяє випадкове використання неправильного значення в іншому файлі того ж пакету.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Виняток: не експортовані значення помилок повинні мати префікс err
без підкреслення.
Див. Іменування помилок.
#Вбудовування в структури (embedding)
Вбудовані типи повинні бути у верхній частині списку полів структури, також повинен бути порожній рядок, який відокремлює вбудовані поля від звичайних полів.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Вбудовування має забезпечувати відчутні переваги, як-от додавання або розширення функціональності семантично прийнятним способом. Це повинно робитись без негативних наслідків для користувача (див. також: Уникайте вбудовування типів у публічні структури).
Виняток: м’ютекси (mutex) не можна вбудовувати, навіть у не експортовані типи. Дивіться також: М’ютекси (mutex) з нульовими значеннями правильні.
Вбудовування не повинно:
- Бути суто косметичними або орієнтованими лише на зручність.
- Робити зовнішні типи більш складними для створення або використання.
- Впливати на нульові значення зовнішніх типів. Якщо зовнішній тип має корисне нульове значення, то вбудовування внутрішнього типу не повинно це змінити.
- Робити публічними функції або поля внутрішнього типу, які жодним чином не пов’язані із зовнішнім типом.
- Розкривати не експортовані типи.
- Впливати на семантику копіювання зовнішніх типів.
- Змінювати API або семантику зовнішнього типу.
- Вставляти неканонічну форму внутрішнього типу.
- Розкривати деталі реалізації зовнішнього типу.
- Дозволяти користувачам спостерігати або контролювати внутрішні елементи.
- Змінювати загальну поведінку внутрішніх функцій, обгорнувши неочікуваними для користувача способами.
Простіше кажучи, робіть вбудовування свідомо та цілеспрямовано. Хорошим лакмусовим папірцем є запитати себе: “чи всі ці експортовані внутрішні методи/поля будуть додані безпосередньо до зовнішнього типу?”; якщо відповідь “деякі” або “ні”, не вставляйте внутрішній тип, замість цього використовуйте поле.
Не рекомендовано | Рекомендовано |
---|---|
|
|
|
|
|
|
#Оголошення локальних змінних
Короткі оголошення змінних (:=
) повинні використовуватися, якщо змінна має явне значення.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Однак існують випадки, коли значення за замовчуванням виглядає зрозумілішим,
якщо використовується ключове слово var
. Наприклад, Оголошення порожніх зрізів.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#nil це повноцінний зріз
nil
- дійсне значення зрізу нульової довжини. Це означає, що,
-
Не слід явно повертати зріз нульової довжини. Натомість повертайте
nil
.Не рекомендовано Рекомендовано 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{} // або, 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) }
Пам’ятайте, що хоча nil є дійсним зрізом, але це не еквівалентно зрізу, якому задано нульову довжину. Перший зріз nil, а другий ні – тому в різних ситуаціях вони можуть розглядатися по-різному (наприклад, серіалізація).
#Зменште область видимості змінних
Де це можливо, зменшуйте область видимості змінних. Дане правило не повинно суперечити наступному Зменште вкладення.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Якщо вам потрібен результат функції поза межами if, тоді вам не слід намагатися зменшувати область видимості.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Уникайте відкритих параметрів
Відкриті параметри у викликах функцій можуть погіршити читабельність.
Додайте коментарі у стилі мови C (/* ... */
) для імен параметрів, якщо їхнє значення неочевидне.
Не рекомендовано | Рекомендовано |
---|---|
|
|
А ще краще, замініть відкриті типи bool
на власні типи для більш читабельного та безпечного коду.
Також це дозволить більше ніж два стани (true/false) для цього параметра в майбутньому.
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady Status = iota + 1
StatusDone
// Можливо в майбутньому ми матимемо StatusInProgress.
)
func printInfo(name string, region Region, status Status)
#Використовуйте необроблені рядкові літерали, щоб уникнути екранування
Go підтримує необроблені рядкові літерали (raw string literals), які можуть охоплювати кілька рядків та містити лапки. Використовуйте їх, щоб уникнути ручного додавання екранованих символів, яке значно погіршує читабельність.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Ініціалізація структур
#Використовуйте імена полів для ініціалізації структур
Ви майже завжди повинні вказувати імена полів під час ініціалізації структур.
Це обов’язково при використанні go vet
.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Виняток: назви полів можна опускати в тестових таблицях, якщо є 3 або менше полів.
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
#Пропускайте поля з нульовими значеннями в структурах
Під час ініціалізації структур з іменами полів, не додавайте поля, які мають нульові значення, якщо вони не несуть змістовний контекст. В іншому випадку дозвольте Go автоматично встановити ці поля з нульовими значеннями.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Це допомагає зменшити шум для читачів, пропускаючи значення за замовчуванням у цьому контексті. Вказуються лише поля, які мають значення.
Додайте нульові значення, якщо назви полів надають змістовний контекст. Наприклад, тестові приклади в тестових таблицях можуть бути корисними з іменами полів, навіть якщо вони мають нульове значення.
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
#Використовуйте var
для структур з нульовим значенням
Якщо всі поля структури опущені в декларації, використовуйте форму var
,
щоб оголосити структуру.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Це відрізняє структури з нульовим значенням від структур з ненульовими полями, подібно до розрізнення, створеного для ініціалізації карти, і відповідає тому, як ми вважаємо за краще оголошувати порожні зрізи.
#Ініціалізація структурних посилань
Declaring Empty Slices
Використовуйте &T{}
замість new(T)
під час ініціалізації посилань на структуру,
щоб це було відповідно до ініціалізації структури.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Ініціалізація карт (maps)
Надавайте перевагу функції make(..)
для порожніх карт і карт, що заповнені програмно.
Це робить ініціалізацію карт візуально відмінним від оголошення,
а також дозволяє простіше вказувати розмір карти пізніше, якщо доступно.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Оголошення та ініціалізація візуально схожі. |
Оголошення та ініціалізація візуально відрізняються. |
Якщо це можливо, вказуйте місткість під час ініціалізації карт за допомогою функції make()
.
Додаткову інформацію дивіться в розділі Вказуйте місткість для карт.
З іншого боку, якщо карта містить фіксований список елементів, використовуйте літерали карти, щоб ініціалізувати карту.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Основне правило полягає в тому, щоб використовувати літерали карт під час
додавання фіксованого набору елементів при ініціалізації, інакше використовуйте
функцію make
та вкажіть місткість (capacity), якщо вона доступна.
#Форматування рядків поза Printf
Якщо ви оголошуєте форматування рядків для функцій у стилі Printf
поза рядковим літералом,
винесіть їх значення в const
.
Це допомагає go vet
виконувати статичний аналіз форматування рядка.
Не рекомендовано | Рекомендовано |
---|---|
|
|
#Назви функцій у стилі Printf
Коли ви оголошуєте функцію у стилі Printf
, переконайтеся, що go vet
може її виявити
та перевірити форматування рядка.
Це означає, що ви повинні використовувати попередньо визначені назви функцій
у стилі Printf
, якщо це можливо. go vet
перевірить їх за замовчуванням.
Для отримання додаткової інформації дивіться Printf family.
Якщо використання попередньо визначених імен неможливе, завершіть вибране ім’я символом f:
Wrapf
, а не Wrap
. go vet
можна попросити перевірити конкретні назви у стилі Printf
,
але вони мають закінчуватися на f.
$ go vet -printfuncs=wrapf,statusf
Також дивіться go vet: Printf family check.
#Шаблони
#Тестові таблиці
Використовуйте тести на основі таблиць з під-тестами, щоб уникнути дублювання коду, коли основна логіка тестування повторюється.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Тестові таблиці спрощують додавання контексту до повідомлень про помилки, зменшують дублювання логіки та додають нові тестові випадки (test cases).
Ми дотримуємося домовленості, згідно з якою зріз структур називається тестами
,
а кожен тестовий випадок — tt
. Крім того, ми заохочуємо пояснювати вхідні та вихідні
значення для кожного тесту з префіксами give
та want
.
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
Паралельні тести, як і деякі спеціалізовані цикли (наприклад, ті, що породжують горутини або захоплюють посилання як частину тіла циклу), мають подбати про те, щоб явно призначити змінні циклу в межах циклу, щоб гарантувати, що вони зберігають очікувані значення.
tests := []struct{
give string
// ...
}{
// ...
}
for _, tt := range tests {
tt := tt // for t.Parallel
t.Run(tt.give, func(t *testing.T) {
t.Parallel()
// ...
})
}
У наведеному вище прикладі ми повинні оголосити змінну tt
, обмежену ітерацією циклу,
через використання t.Parallel()
нижче.
Якщо ми цього не зробимо, більшість або всі тести отримають неочікуване значення для tt
або значення, яке змінюється під час їх виконання.
#Функціональні параметри
Функціональні параметри – це шаблон, у якому ви оголошуєте непрозорий тип Option
,
який записує інформацію в деякій внутрішній структурі. Ви приймаєте різноманітну
кількість цих параметрів і дієте відповідно до повної інформації,
записаної параметрами у внутрішній структурі.
Використовуйте цей шаблон для додаткових аргументів у конструкторах та інших загальнодоступних API, які, як ви передбачаєте, потребують розширення, особливо якщо у вас уже є три або більше аргументів для цих функцій.
Не рекомендовано | Рекомендовано |
---|---|
|
|
Параметри cache та logger мають бути надані завжди, навіть якщо користувач хоче використовувати параметри за замовчуванням.
|
Параметри надаються лише за потреби.
|
Наш запропонований спосіб реалізації цього шаблону полягає в інтерфейсі Option
,
який містить не експортований метод, записуючи параметри в не експортовану структуру options
.
type options struct {
cache bool
logger *zap.Logger
}
type Option interface {
apply(*options)
}
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
func WithCache(c bool) Option {
return cacheOption(c)
}
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
// Open створює з'єднання.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
// ...
}
Зауважте, що існує метод реалізації цього шаблону за допомогою замикань, але ми вважаємо,
що наведений вище шаблон забезпечує більшу гнучкість для авторів і легше налагоджувати та
тестувати для користувачів. Зокрема, це дозволяє порівнювати параметри один з одним у тестах
та макетах (mocks), проти замикань, де це неможливо. Крім того, це дозволяє опціям реалізувати
інші інтерфейси, включаючи fmt.Stringer
, який дозволяє зрозумілі для користувача рядкові
представлення параметрів.
Дивіться також,
#Linting
Значно важливіше, ніж будь-який “благословенний” набір лінтерів, послідовне розміщення лінту в кодовій базі.
Ми рекомендуємо використовувати принаймні такі лінтери, оскільки вважаємо, що вони допомагають виявити найпоширеніші проблеми, а також встановлюють високу планку якості коду:
-
errcheck - щоб переконатися, що помилки обробляються
-
goimports - для форматування коду та керування імпортом
-
golint - щоб вказати на типові помилки стилю
-
govet - для аналізу коду на наявність типових помилок
-
staticcheck - для виконання різних перевірок статичного аналізу
#Lint Runners
Ми рекомендуємо golangci-lint як засіб запуску лінтів для коду Go, головним чином завдяки його продуктивності у великих базах коду, а також можливості налаштовувати та використовувати багато канонічних лінтерів одночасно. Цей репозиторій містить приклад конфігураційного файлу .golangci.yml із рекомендованими лінтерами та їх налаштуваннями.
golangci-lint має різні лінтери, що доступні для використання. Наведені вище лінтери рекомендовано як базовий набір і ми заохочуємо команди додавати будь-які додаткові лінтери, які потрібні для їхніх проектів.