Перейти до змісту

Домовленості проєкту

gofmt вирішує питання форматування, а go vet ловить певний клас помилок. Що лишається — це набір звичок, на які погоджується команда — речі, які жоден інструмент не перевіряє, і саме тому їх варто записати.

if err != nil {
    return fmt.Errorf("loading user %d: %w", id, err)
}

Обгортайте кожну помилку контекстом

Помилка, що доходить до логу як sql: no rows in result set, не каже нічого про те, який саме SQL-запит, який id чи яке звернення. Обгортайте на кожному рівні, що додає інформацію:

return fmt.Errorf("loading user %d: %w", id, err)

Домовленості, що роблять обгорнуті помилки читабельними:

  • Малі літери, без крапки в кінці. Помилки конкатенуються; Failed to load user. всередині іншого повідомлення читається погано.
  • Без "failed to". Те, що це помилка, вже сказано самим фактом. loading user 42: connection refused краще, ніж failed to load user 42: failed to connect: connection refused.
  • %w, не %v, якщо тільки ви свідомо не хочете розірвати ланцюжок. %v сплющує помилку в текст, і errors.Is перестає працювати.
  • Додавайте щось. Обгортання без нової інформації — це шум; краще передайте помилку вгору незмінною.

Ніколи не відкидайте помилку мовчки. Якщо вона справді не має значення, скажіть це:

_ = resp.Body.Close()   // намагаємось, без гарантій

Явне _ каже рецензенту, що це було рішення.

ctx першим, завжди

Кожна функція, що виконує введення-виведення чи може заблокуватися, приймає контекст першим параметром, названим ctx:

func (s *store) ByID(ctx context.Context, id int64) (User, error)

Послідовність важливіша за окремий випадок. Якщо половина функцій його приймає, інша половина стає тими, які доводиться щоразу вишукувати.

Рівні логування, що дещо означають

Рівні корисні, лише якщо їх використовують однаково всюди:

Рівень Значення
Error людина має діяти; щось зламане
Warn деградовано, але оброблено — спрацював фолбек, відбулася повторна спроба
Info значуща подія: запущено, зупинено, завдання завершено
Debug деталь для діагностики, вимкнено в продакшні

Два правила, що рятують більше, ніж ця таблиця. Логуйте помилку один раз, на тому рівні, що її обробляє — логування й повернення на кожному рівні дає п'ять записів на один збій. І не логуйте на рівні Error те, що ви також повертаєте: виклик залогує це сам, і тепер у вас той самий збій двічі з різним контекстом.

Ніколи не логуйте секрети, токени чи персональні дані. Це включає %+v на структурі, що випадково містить поле пароля.

Коментуйте чому, а не що

// PreferSimpleProtocol уникає кешу підготовлених запитів, який
// пулер з'єднань перед цією базою даних не підтримує.
PreferSimpleProtocol: true,

Код каже що. Коментар заслуговує на своє місце, пояснюючи те, чого код не може: обхідний шлях, неочевидне обмеження, рішення, що виглядає хибним, але таким не є. Коментарі, що переказують рядок нижче, гниють у мить, коли цей рядок зміниться.

Експортовані ідентифікатори отримують doc-коментар, що починається з назви:

// ByID повертає користувача з даним id, або ErrUserNotFound.
func (s *store) ByID(ctx context.Context, id int64) (User, error)

Саме таку форму рендерять go doc і pkg.go.dev.

Обґрунтовуйте сирий SQL

Там, де в проєкті є стандартний спосіб дістатися бази даних, а ви з нього виходите, залиште причину:

// Raw: згенерований стовпець — fts_content обчислюється автоматично,
// тож INSERT має явно називати свої стовпці, а не покладатися на ORM.

Цінність не в самому коментарі; вона в тому, що рецензент бачить, чи цей виняток — один із тих кількох, які команда приймає, чи це просто скорочення шляху. Та сама звичка стосується будь-якого запобіжного клапана: директиви //nolint, виклику unsafe, сну в тесті. Запобіжні клапани без причин множаться.

Іменування

  • Короткі назви для коротких областей видимості. i, r, w, db — добре всередині п'ятирядкової функції й погано як ідентифікатори рівня пакета.
  • Без затинання. store.New, не store.NewStore; http.Client, не http.HTTPClient.
  • Інтерфейси описують поведінку. Reader, UserStore — не IUserStore чи UserStoreInterface.
  • Отримувачі — одна чи дві літери, послідовні на кожному методі типу.
  • Абревіатури зберігають регістр: userID, ServeHTTP, parseURL — ніколи не userId чи parseUrl.

Приймайте інтерфейси, повертайте структури

Приймайте як параметр найвужчий потрібний інтерфейс; повертайте конкретний тип. Виклики зберігають повну інформацію, а ви вільні додавати методи пізніше, нікого не ламаючи.

Наслідок: не створюйте інтерфейс, доки не з'явиться друга реалізація чи тест, якому він потрібен. Інтерфейс з однією реалізацією — це непрямість без вигоди.

Нульові значення мають працювати

Тип, чиє нульове значення придатне до використання, прибирає цілий клас конструкторів:

var buf bytes.Buffer   // готовий
var mu sync.Mutex      // готовий

Проєктуйте під це, де можете. Де не можете, зробіть це очевидним — неекспортоване поле, яке конструктор мусить встановити, щоб нульове значення провалювалося гучно, а не поводилося тонко неправильно.

Де це записувати

CONTRIBUTING.md, якого ніхто не читає, гірший за його відсутність. Що працює:

  • Робіть механічним, де можливо. Усе, що може перевірити лінтер, має перевіряти лінтер, а не рецензенти.
  • Пояснюйте чому. Правило з причиною виживає; правило без причини обговорюють знову кожні пів року.
  • Тримайте коротким. Десять правил, яких дотримуються, кращі за п'ятдесят, які лише проглядають.

З досвіду Python: gofmt завершує дискусію про форматування, яку завершив black, тільки раніше й узагалі без конфігурації. Обгортання помилок — це еквівалент raise ... from err, тільки робити це доводиться вручну щоразу — така ціна того, що помилки є значеннями.

Швидка довідка

Домовленість Форма
обгортати помилки fmt.Errorf("doing thing: %w", err) — малі літери, без "failed to"
ігнорувати помилку _ =, свідомо
контекст ctx context.Context першим, завжди
логувати збій один раз, там, де його обробляють
коментарі чому, а не що; doc-коментарі починаються з назви
запобіжні клапани залишити письмову причину
назви коротка область видимості — коротка назва; без затинання; ID, URL, HTTP
інтерфейси приймати вузькі, повертати конкретні типи
нульові значення робити придатними до використання, де можете

Джерела