1. Чому в Go так багато if err != nil
Коли ви вперше бачите Go-код, може здатися, ніби мову спеціально створили, щоб тренувати терпіння: виклик → if err != nil → повернення → виклик → if err != nil → повернення… І ось ви вже майже готові написати макрос CHECK_ERR() і жити спокійно. Але в Go макросів немає — бо життя має бути чесним.
Насправді цей стиль — свідомий вибір. Go заохочує явне керування потоком виконання у разі помилок. Тобто помилка не «стрибає» через стек сама, а стає значенням, з яким ви працюєте як із будь-яким іншим. В офіційних матеріалах про «помилки як значення» цю думку формулюють прямо: важливо не позбутися перевірок, а зробити так, щоб програма коректно перевіряла помилки, не ховаючи контроль потоку за магією.
Звідси й з’являється наша сьогоднішня ідіома:
if err != nil {
return ...
}
Її сенс максимально приземлений: якщо крок провалився — далі йти не можна, тож виходимо одразу.
2. Ритм коду: викликав → перевірив → ранній return
Зараз ми зробимо важливий перехід: перестанемо сприймати if err != nil { return ... } як «прикрий обовʼязок» і почнемо бачити в ньому ритм, у якому пишеться Go-код. Це схоже на водіння: спочатку ви дуже свідомо думаєте «поворотник — дзеркало — маневр», а потім це стає звичкою. Приблизно так само й тут: крок — перевірка — або продовжуємо, або виходимо.
Щоб побачити логіку, зручно уявити функцію як послідовність кроків, де кожен може «не спрацювати». Ідіома раннього повернення робить так, що «щасливий шлях» (happy path) читається зверху вниз без зайвої вкладеності.
Ось проста блок-схема:
flowchart TD
A[Крок 1: викликаємо функцію] --> B{err != nil?}
B -- так --> E[return ... err]
B -- ні --> C[Крок 2: наступний виклик]
C --> D{err != nil?}
D -- так --> E
D -- ні --> F[Продовжуємо щасливий шлях]
Саме це й робить if err != nil { return ... }: тримає контроль потоку чесним і читабельним.
3. Ранній return замість драбинки else
Давайте поговоримо про типову альтернативу: ви починаєте писати обробку помилок, але замість ранніх повернень будуєте «драбинку»:
- if err == nil { ... } else { ... }
- всередині if ще if err == nil { ... } else { ... }
- і так далі, аж доки ваш код не стає схожим на багатоповерхівку без ліфта.
Проблема «драбинки» не в тому, що вона «не за стилем». Проблема — у читабельності: ви хочете очима бачити основний сценарій, але змушені продиратися крізь рівні вкладеності. Go-підхід зазвичай інший: помилку перевірили, вийшли, а основний сценарій продовжується на тому самому рівні відступів.
Порівняймо на короткому прикладі. Припустімо, ми хочемо розпарсити два числа й вивести суму.
Варіант із вкладеністю
package main
import (
"fmt"
"strconv"
)
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err == nil {
y, err := strconv.Atoi(b)
if err == nil {
fmt.Println("результат:", x+y) // результат: ...
return nil
}
return err
}
return err
}
Працює, але видно, що «нормальна» логіка затиснута всередині if.
Варіант у стилі Go: ранні повернення
package main
import (
"fmt"
"strconv"
)
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err
}
y, err := strconv.Atoi(b)
if err != nil {
return err
}
fmt.Println("результат:", x+y) // результат: ...
return nil
}
Такий приклад часто наводять як ілюстрацію того, як виглядає типовий Go-код із помилками. Так, рядків і справді більше, але структура ясна: зверху вниз читається «що робимо», а обробка помилок — це «охоронці біля дверей», які не пускають нас далі, якщо щось пішло не так.
4. Що писати в return ... і навіщо zero value
Сама ідіома if err != nil { return ... } майже завжди живе всередині функцій, які повертають помилку назовні. Але тут у новачків виникають два стійкі запитання.
Перше: «Чи можна просто return err?»
Можна, якщо ваша функція повертає лише error. Але часто у нас контракт (T, error), і тоді потрібно повернути два значення.
Друге: «А що повернути замість T, якщо сталася помилка?»
За типовою практикою Go повертають zero value — нульове значення — типу T. Це не магія і не обовʼязковий закон природи, а практичний захист від випадкового використання результату, який насправді невалідний.
Подивіться невеликий приклад: парсимо число з рядка, але хочемо заборонити від’ємні значення.
package main
import (
"fmt"
"strconv"
)
func ParseNonNegativeInt(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
return 0, err
}
if n < 0 {
return 0, fmt.Errorf("очікувалося невідʼємне ціле, отримано %d", n)
}
return n, nil
}
Тут 0 — zero value для int. Ми ніби кажемо коду, який викликає: «результату немає, не чіпайте його — дивіться на err».
5. Правило читабельності: помилка — вихід
Зараз зафіксуємо просте правило, яке допомагає писати й читати Go-код.
Якщо сталася помилка, зазвичай далі в цій функції робити нічого. Отже, гілка err != nil має або повертати керування (return), або різко змінювати потік (наприклад, break у циклі). У межах нашої теми ми зосереджуємося саме на поверненні назовні.
Ця звичка робить дві речі водночас: код стає лінійнішим, тож у ньому менше вкладеності, і стає складніше випадково продовжити роботу з поганими даними.
Є важливий психологічний ефект: коли ви читаєте функцію, ваш мозок швидко вчиться розпізнавати «охоронців»:
if err != nil {
return ...
}
і майже автоматично пропускає їх, розуміючи: це захист, а не основна логіка. У нотатках про обробку помилок у Go підкреслюють саме цей момент: явні перевірки помилок — частина моделі мови, і вони формують зрозумілий контроль потоку.
6. return err і додавання контексту без %w
Дуже часта ситуація: помилка прийшла зсередини (наприклад, із strconv.Atoi), і ви хочете повернути її назовні. Якщо ви просто зробите return 0, err, це коректно. Але іноді ззовні буде незрозуміло, що саме ви намагалися зробити і на яких даних.
Тому в нас є акуратний прийом: додати контекст текстом через fmt.Errorf, не обгортаючи причину в окрему структуру. Це ми сьогодні не чіпаємо.
package main
import (
"fmt"
"strconv"
)
func ParseID(idStr string) (int, error) {
id, err := strconv.Atoi(idStr)
if err != nil {
return 0, fmt.Errorf("не вдалося розібрати id %q: %v", idStr, err)
}
return id, nil
}
Такий стиль «операція — причина» регулярно трапляється в Go-коді. Наприклад, у прикладах із розбору помилок прямо показують додавання контексту через fmt.Errorf("...: %v", err).
7. Патерн run() error і один if err != nil у main()
Тепер давайте перейдемо до практики: як організувати код так, щоб обробка помилок не розмазувалася по всьому main.
Поширений і дуже зручний прийом — зробити функцію run() error, усередині якої вся логіка, а main() перетворити на тонку оболонку: викликати run(), перевірити помилку, вивести повідомлення й завершитися. Це особливо добре тренує саме цю ідіому, бо в run() ви писатимете багато ранніх повернень.
Почнімо з каркаса:
package main
import "fmt"
func run() error {
// тут буде «справжня» логіка
return nil
}
func main() {
if err := run(); err != nil {
fmt.Println("помилка:", err)
return
}
}
Зверніть увагу на запис if err := run(); err != nil { ... }: ми оголосили err прямо всередині if, і він існує лише в межах цього блоку. Це зменшує шанс випадково використати старе значення err десь далі.
8. Кроки всередині run(): приклад TaskBox
Зробімо дуже просту навчальну штуку: програма читає команду зі stdin і виконує один крок. Ми не робимо повноцінний CLI з прапорцями (це буде пізніше), а використовуємо те, що вже вміємо: fmt.Scan.
Підтримаємо дві команди:
- add <title> — додати задачу (в пам’ять),
- get <id> — вивести задачу за індексом.
Щоб поки що не переходити до структур (вони ще попереду), будемо зберігати задачі в []string. Індекс у слайсі й буде нашим id.
Ось заготовка run(), яка читає команду й аргумент:
package main
import "fmt"
func run() error {
var cmd string
var arg string
if _, err := fmt.Scan(&cmd, &arg); err != nil {
return err
}
fmt.Println("команда:", cmd, "аргумент:", arg) // наприклад: команда: add аргумент: Купити_молоко
return nil
}
Тут уже видно ритм: виклик Scan → перевірка → ранній return.
Тепер додамо мінімальну логіку з окремими функціями, щоб помилки було зручно повертати назовні.
Додавання задачі: помилка, якщо назва порожня
package main
import (
"errors"
)
func AddTask(tasks []string, title string) ([]string, error) {
if len(title) == 0 {
return nil, errors.New("назва задачі порожня")
}
tasks = append(tasks, title)
return tasks, nil
}
Зауважте: у разі помилки ми повертаємо nil замість слайса (це zero value для []string) і помилку. Це чесно: «нового стану немає, операцію не виконано».
Отримання задачі: помилка, якщо індекс поза діапазоном
package main
import "fmt"
func GetTask(tasks []string, id int) (string, error) {
if id < 0 || id >= len(tasks) {
return "", fmt.Errorf("ідентифікатор задачі поза діапазоном: %d", id)
}
return tasks[id], nil
}
Тут zero value для string — порожній рядок "".
9. run() як оркестратор: перевіряємо помилку після кожного кроку
Тепер найважливіше: покажімо «оркестраторський» стиль Go, де run() викликає кроки один за одним і після кожного робить саме те, про що йдеться в лекції.
package main
import (
"fmt"
"strconv"
)
func run() error {
tasks := []string{"Вивчити Go"} // стартові дані, щоб було що отримувати
var cmd, arg string
if _, err := fmt.Scan(&cmd, &arg); err != nil {
return err
}
if cmd == "add" {
var err error
tasks, err = AddTask(tasks, arg)
if err != nil {
return err
}
fmt.Println("готово") // готово
return nil
}
if cmd == "get" {
id, err := strconv.Atoi(arg)
if err != nil {
return fmt.Errorf("не вдалося розібрати id %q: %v", arg, err)
}
task, err := GetTask(tasks, id)
if err != nil {
return err
}
fmt.Println(task) // наприклад: Вивчити Go
return nil
}
return fmt.Errorf("невідома команда %q", cmd)
}
Тут багато «маленьких» if err != nil { return ... }, але це якраз і є «правильна нудьга» Go: кожен крок або пройшов, або ми чесно виходимо.
10. Корисні мікропатерни та антипатерни
Мікропатерн: if _, err := ...; err != nil { return ... }
Іноді функція повертає корисне значення й error, але саме значення вам не потрібне. Наприклад, ви щось друкуєте або виконуєте дію заради ефекту. У таких випадках зручно використовувати підкреслення _ і коротке оголошення err прямо в if.
package main
import "fmt"
func PrintHeader() error {
if _, err := fmt.Println("=== TaskBox ==="); err != nil {
return err
}
return nil
}
Цей патерн робить дві корисні речі: він не засмічує функцію зайвими змінними й одразу показує читачеві: «важлива лише помилка».
Антипатерн: «надрукувати err і йти далі»
Дуже типова помилка новачка: він побачив err, злякався, надрукував його… і продовжив виконувати код, ніби все нормально. Це особливо небезпечно у функціях, де результат у разі помилки перетворюється на zero value. Наприклад, strconv.Atoi("x") поверне 0 і помилку. Якщо ви проігноруєте помилку, у вас раптово зʼявиться «валідне» число 0, і далі ви обчислюватимете щось зовсім не те.
Ідіома if err != nil { return ... } якраз лікує цю хворобу: у ній «правильна реакція» на помилку вбудована в структуру коду. Ви або обробили проблему й змінили потік, або вийшли назовні, щоб її обробив хтось вище.
11. Типові помилки
Помилка № 1: перевірити err, але не змінити потік виконання.
Часто пишуть fmt.Println(err) всередині if err != nil, а потім продовжують роботу з даними, які насправді невалідні. Якщо помилка означає, що крок не виконано, то після перевірки зазвичай потрібен ранній вихід: return err (або інший явний вихід із поточного сценарію).
Помилка № 2: переплутати умову й обробити помилку в гілці err == nil.
Це виглядає смішно, доки не стається вночі в пʼятницю перед дедлайном. Тримайте в голові просту «мантру»: err != nil — це гілка проблеми, err == nil — це гілка успіху. Якщо ви пишете раннє повернення, то майже завжди умова саме err != nil.
Помилка № 3: використовувати результат до перевірки помилки.
У контрактах (T, error) значення T у разі помилки часто дорівнює zero value. Воно виглядає «нормальним» і тому особливо небезпечне. Правильний порядок у Go майже ритуальний: викликав → перевірив err → лише потім використовуєш T.
Помилка № 4: повертати «напіврезультат» разом із помилкою й сподіватися, що викликач «якось розбереться».
Іноді хочеться повернути «те, що встигли порахувати», і паралельно помилку. На рівні новачка це зазвичай призводить до того, що результат використовують і забувають перевірити err. У базовому стилі Go безпечніше повертати zero value результату в разі помилки, щоб неправильне використання було менш імовірним.
Помилка № 5: нагромаджувати вкладеність замість ранніх повернень.
Коли ви обгортаєте кожен крок у if err == nil { ... }, ви отримуєте драбинку відступів, і «нормальний сценарій» ховається всередину. Ранній return якраз потрібен, щоб happy path лишався «по прямій», без тунелів і підземних рівнів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ