1. Навіщо в Go так часто перевіряють err
Коли ви пишете програму, світ починає підкидати їй бананові шкірки. Користувач вводить не те. Файл не знайдено. Число надто велике. Усе може піти не так. У Go такі ситуації заведено не замовчувати, а обробляти там, де вони виникають, — короткою й зрозумілою перевіркою.
Саме тому конструкція if err != nil { ... } трапляється в Go-коді постійно: це не «шаблон для галочки», а спосіб тримати програму в нормальному стані. В офіційних матеріалах Go це описують як базовий стиль: помилки — це значення, і працюють з ними явно, без магії.
error і nil: проста модель
Зараз нам не потрібно заглиблюватися в інтерфейси або розбиратися, як саме тип error влаштований зсередини. Нам потрібна практична модель рівня «можу писати програми й не плакати». У Go багато функцій повертають error, і це значення буває або nil, або не nil.
Якщо err == nil, це означає «помилки немає, усе нормально». Якщо err != nil, це означає «щось пішло не так, далі не можна вдавати, що все добре». Така перевірка — найпоширеніший спосіб зрозуміти, чи успішною була операція. У вступних матеріалах про помилки в Go це формулюють прямо: найзвичніша перевірка — порівняти помилку з nil.
Міні-демо: помилка — це значення, його можна друкувати.
package main
import "fmt"
func main() {
var err error
fmt.Println(err == nil) // true
}
Так, це виглядає надто просто. Але ця простота — як ремінь безпеки: не робить поїздку веселішою, зате рятує, коли «прилітає».
2. Помилки у введенні та парсингу: Scan і Atoi
До цього моменту могло здаватися, що введення й перетворення — це майже завжди безпечна справа. На тестах ви вводите 10, і все чудово. Але програма живе не лише у ваших руках: користувач може ввести "ten", 10.5, порожній рядок або взагалі надіслати EOF — тобто сигнал про завершення введення. І саме тут помилки починають траплятися регулярно.
У наших задачах є два класичних джерела помилок. По-перше, fmt.Scan може не прочитати те, що ви очікуєте, і повернути err. По-друге, strconv.Atoi може не зуміти розібрати рядок як ціле число й теж поверне помилку. Тож без перевірки err ви насправді не знаєте, чи коректні дані лежать у змінних, — ви лише сподіваєтеся. А надія, як відомо, погана стратегія тестування.
Приклад із Scan:
package main
import "fmt"
func main() {
var x int
_, err := fmt.Scan(&x)
if err != nil {
fmt.Println("не вдалося прочитати число") // наприклад, якщо ввели "abc"
return // вихід із main()
}
fmt.Println("x =", x)
}
3. Патерн: викликав → перевірив err → використовуй результат
Є дуже корисне правило: якщо функція повертає error, перевіряйте його одразу. Не «в кінці програми», не «після ще кількох рядків», не «коли стане зрозуміло». Одразу. Інакше ви починаєте писати код, який використовує потенційно неправильні дані, а потім намагаєтеся лікувати симптоми.
Ця ідея настільки фундаментальна, що в багатьох обговореннях Go-помилок повторюється як мантра: помилка — це частина нормального потоку керування, просто окрема гілка.
Подивімося на мікрошаблон:
package main
import (
"fmt"
"strconv"
)
func main() {
s := "42x"
n, err := strconv.Atoi(s)
if err != nil {
fmt.Println("це не число:", s) // це не число: 42x
return
}
fmt.Println("n + 1 =", n+1)
}
Зверніть увагу на важливу «психологію» коду: доки err != nil, ми не робимо вигляд, що число вдалося. Ми обираємо безпечний шлях: повідомляємо користувача й виходимо.
if з ініціалізатором: менше «зайвих» змінних
Іноді початківці пишуть так: оголошують змінну n, потім err, потім ще щось — і раптом main перетворюється на склад із коробками. У Go є зручна форма: ініціалізатор у if, яка дозволяє створити тимчасові змінні прямо всередині умови й обмежити їхню область видимості.
Це особливо зручно для парсингу й читання: «спробували → перевірили → або помилка, або працюємо далі». І головне: змінні n і err не розповзаються по всьому main, а живуть рівно там, де потрібні.
Приклад:
package main
import (
"fmt"
"strconv"
)
func main() {
if n, err := strconv.Atoi("100"); err != nil {
fmt.Println("помилка парсингу:", err)
} else {
fmt.Println(n + 1) // 101
}
}
Тут важливо пам’ятати: n і err існують тільки всередині if/else. Якщо вам потрібно число далі по програмі — оголошуйте змінну заздалегідь (ми це вже обговорювали в темі областей видимості).
4. Приклад: калькулятор рахунку та чайових
Зараз зберемо невеликий консольний застосунок, який робить корисну побутову річ: рахує підсумкову суму за рахунком з урахуванням чайових. Приклад навмисно спрощений: що ближче до реальності, то швидше мозок перестає сприймати err як «щось абстрактне з підручника» і починає сприймати його як «спосіб не зламати програму».
У міру покращень ми не вигадуватимемо нових конструкцій, а застосуємо те, що вже знаємо: fmt.Scan, Atoi, if/else і наш сьогоднішній герой — if err != nil. У результаті вийде програма, яка не падає від першого ж кривого введення.
Версія 1: «наосліп»
Почнемо з версії, яка компілюється і навіть інколи працює. Вона читає два значення: суму та відсоток чайових. Потім намагається перетворити їх на числа. Помилки ми ігноруємо, бо «ну, користувач же нормальна людина… мабуть».
package main
import (
"fmt"
"strconv"
)
func main() {
var billStr, tipStr string
fmt.Scan(&billStr, &tipStr)
bill, _ := strconv.Atoi(billStr)
tipPercent, _ := strconv.Atoi(tipStr)
total := bill + bill*tipPercent/100
fmt.Println(total)
}
Проблема цієї версії не в тому, що вона «погана за стилем». Проблема в тому, що вона буквально вводить вас в оману: якщо користувач увів billStr = "abc", то Atoi поверне помилку, а bill стане 0. І ви отримаєте «підсумок 0», хоча насправді треба було сказати: «введення некоректне».
Версія 2: перевіряємо Scan і Atoi
Тепер зробімо по-дорослому: після кожної потенційно аварійної операції перевіряємо помилку. Причому окремо обробляємо помилки читання й помилки перетворення — це різні проблеми, і користувачеві важливо бачити, де саме все зламалося.
package main
import (
"fmt"
"strconv"
)
func main() {
var billStr, tipStr string
_, err := fmt.Scan(&billStr, &tipStr)
if err != nil {
fmt.Println("помилка введення: потрібно два значення")
return
}
bill, err := strconv.Atoi(billStr)
if err != nil {
fmt.Println("сума має бути цілим числом")
return
}
tipPercent, err := strconv.Atoi(tipStr)
if err != nil {
fmt.Println("чайові мають бути цілим числом")
return
}
total := bill + bill*tipPercent/100
fmt.Println("разом:", total)
}
Зверніть увагу на ефект: код став довшим, але поведінка — передбачуванішою. Ми більше не «вгадуємо», що лежить у bill. Ми точно знаємо: або там число, або ми вже вийшли з програми.
І саме тому патерн if err != nil такий поширений: Go віддає перевагу явності й контролю потоку виконання. В обговореннях Go-помилок підкреслюють, що такий стиль може здаватися шумним, але він робить місця можливого збою видимими просто в коді.
Версія 3: додаємо валідацію
Є ще один важливий момент. Помилка (err) каже: «операція не вдалася». Але навіть якщо операція вдалася, дані можуть бути логічно неправильними. Наприклад, чайові -10% або сума рахунку -500. Це не помилка Atoi — це помилка змісту.
Валідація робиться звичайним if, бо це вже наша бізнес-логіка: які значення ми вважаємо допустимими.
package main
import (
"fmt"
"strconv"
)
func main() {
var billStr, tipStr string
_, err := fmt.Scan(&billStr, &tipStr)
if err != nil {
fmt.Println("помилка введення: потрібно два значення")
return
}
bill, err := strconv.Atoi(billStr)
if err != nil {
fmt.Println("сума має бути цілим числом")
return
}
if bill < 0 {
fmt.Println("сума не може бути від’ємною")
return
}
tipPercent, err := strconv.Atoi(tipStr)
if err != nil {
fmt.Println("чайові мають бути цілим числом")
return
}
if tipPercent < 0 {
fmt.Println("відсоток чайових не може бути від’ємним")
return
}
total := bill + bill*tipPercent/100
fmt.Println("разом:", total)
}
Тут у нас з’являється дуже здорова структура: спочатку розбираємося, чи введення взагалі читається, потім — чи воно парситься, далі — чи має сенс, і лише після цього рахуємо результат.
Схема потоку: де живе if err != nil
Щоб не сприймати перевірки як «перешкоди між рядками», корисно побачити їх як розвилку потоку виконання. Ось та сама схема read → parse → compute → print, але з нормальною гілкою помилок:
flowchart TD
A[читання: fmt.Scan] -->|err == nil| B[парсинг: strconv.Atoi]
A -->|err != nil| E[повідомити про помилку й вийти]
B -->|err == nil| C[обчислення]
B -->|err != nil| E
C --> D[виведення результату]
І ось чому Go любить if err != nil: він перетворює «невдачу» на звичайну, читабельну гілку — без прихованих стрибків і несподіванок.
Чому в Go немає try/catch: користь явності
У багатьох, хто приходить у Go з мов із винятками, перша реакція на if err != nil — «чому так багатослівно?». Реакція зрозуміла: у звичних мовах можна написати «зроби раз-два-три, а якщо що — злови виняток». У Go підхід інший: помилки — це значення, а програміст сам вирішує, що з ними робити.
Так, перевірок може бути багато. У дискусіях про синтаксичну підтримку помилок, яку час від часу пропонують, визнають, що шаблон виду x, err := call(); if err != nil { ... } справді може засмічувати корисний код, особливо коли обробка помилок примітивна. Але в цього є практична перевага: у кожному місці, де щось може зламатися, у коді стоїть явний «маячок». У результаті, читаючи програму, ви заздалегідь бачите всі точки ризику.
А ще є дуже приземлений бонус: якщо ви дисципліновано перевіряєте err одразу, то помилки частіше залишаються локальними й зрозумілими. Ви не ловите загадкові наслідки «десь раніше щось пішло не так», а зупиняєтеся на першому ж проблемному місці, поки контекст ще свіжий.
5. Типові помилки під час роботи з if err != nil
Коли ви тільки починаєте писати перевірки помилок, мозок інколи сприймає їх як «шум» і намагається їх «проскочити». Це нормально: звичка формується через повторення. Нижче — найчастіші граблі, на які наступають новачки, і чому вони справді небезпечні.
Помилка №1: ігнорувати err, бо «у прикладах усе працювало».
Поки ви тестуєте на ідеальному введенні, здається, що помилки — це рідкість. Але щойно введення робить людина (або автотест, який спеціально намагається зламати програму), err перетворюється з «формальності» на єдиний спосіб не отримати сміттєві дані. Ігнорування помилки в Go майже завжди означає, що ви дозволили програмі продовжувати працювати в невідомому стані.
Помилка №2: перевіряти помилку, але все одно використовувати результат, ніби все гаразд.
Іноді трапляється логіка: «якщо помилка — надрукую попередження… але все одно порахую». Це майже завжди призводить до дивних підсумків. Якщо парсинг не вдався, число у змінній зазвичай буде 0 (або інше zero value), і ви отримаєте правдоподібний, але неправильний результат. Якщо вже перевірили err != nil, то в цій гілці потрібно припиняти основний сценарій: return, альтернативне значення, повторне введення — що завгодно, але не «продовжуємо, ніби нічого не сталося».
Помилка №3: перевіряти err занадто пізно.
Якщо ви викликали Atoi, потім зробили ще кілька обчислень, а потім перевірили err, то ви вже встигли збудувати дім на піску. Правильна звичка — перевіряти одразу, доки ви точно знаєте, до якого виклику належить помилка і які дані можна вважати валідними.
Помилка №4: переплутати err == nil і err != nil.
Це звучить смішно (і трохи боляче), але на практиці трапляється постійно: особливо коли людина втомилася і вже десятий раз пише однотипний код. Хороший прийом — проговорювати вголос: «якщо err не nil, значить є помилка». І писати обробку помилки в цій гілці максимально коротко, щоб не заплутатися.
Помилка №5: змішувати «помилку операції» і «погані дані за змістом».
Atoi відповідає лише за те, чи є рядок числом. Він не знає про бізнес-обмеження на кшталт «не може бути відʼємним» або «має бути в межах дозволеного діапазону». Тому після успішного парсингу потрібні окремі if для логічної перевірки значень. Це не заміна err-перевірці, а наступний шар захисту.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ