JavaRush /Курси /Go SELF /Створення помилок: errors.New і fmt.Errorf

Створення помилок: errors.New і fmt.Errorf

Go SELF
Рівень 16 , Лекція 1
Відкрита

1. Навіщо повертати помилки, а не друкувати повідомлення

Коли ви тільки починаєте, дуже хочеться діяти «по-людськи»: якщо щось пішло не так — надрукувати повідомлення й іти далі. Проблема в тому, що комп’ютер не людина: він не розуміє натяків. Якщо функція не змогла виконати роботу, вона має явно повідомити про це коду, який її викликав, щоб той міг зупинитися, попросити користувача ще раз ввести дані або вибрати інший шлях.

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

Інструменти: errors.New і fmt.Errorf

Тепер розглянемо два головні інструменти цього уроку.

  • errors.New("текст") створює просту помилку з фіксованим повідомленням.
  • fmt.Errorf("формат %d", x) створює помилку, але дає змогу підставити значення в текст, щоб повідомлення було конкретнішим і кориснішим для діагностики.

Важливо: у цій лекції ми використовуємо fmt.Errorf без %w. Тобто ми не будуємо ланцюжки причин і не робимо вкладену помилку частиною контракту (API). Ми розв’язуємо простішу задачу: «створи помилку» або «створи помилку з контекстом у тексті».

2. errors.New: фіксоване повідомлення

errors.New — це найпряміший спосіб сказати: «сталося щось погане». Він приймає рядок і повертає значення типу error. Підходить для випадків, коли причина зрозуміла і не потребує підстановок: "ділення на нуль", "порожній ввід", "некоректна операція".

Мініприклад: функція ділення, яка відмовляється ділити на нуль.

package main

import (
	"errors"
	"fmt"
)

func Divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("ділення на нуль")
	}
	return a / b, nil
}

func main() {
	q, err := Divide(10, 0)
	fmt.Println(q, err) // 0 ділення на нуль
}

Тут важливо побачити дві речі. По-перше, у разі помилки ми повернули 0 — це zero value для int. По-друге, ми повернули не-nil-значення для помилки, щоб код, який викликає функцію, міг ухвалити рішення.

3. fmt.Errorf: помилка з контекстом

Щойно в помилці вам хочеться побачити конкретне значення, errors.New починає програвати. Повідомлення "некоректне введення" — це як «у вас щось зламалося» без уточнень: так, дякую, я й так це бачу. У цей момент на сцену виходить fmt.Errorf: він форматує рядок за правилами fmt.Printf і повертає його як error.

Наприклад, порахуємо відсоток і явно повідомимо, що total виявився нулем:

package main

import "fmt"

func Percent(part, total int) (int, error) {
	if total == 0 {
		return 0, fmt.Errorf("відсоток: total дорівнює 0 (part=%d)", part)
	}
	return part * 100 / total, nil
}

func main() {
	p, err := Percent(5, 0)
	fmt.Println(p, err) // 0 відсоток: total дорівнює 0 (part=5)
}

Повідомлення стало конкретним: видно і операцію ("відсоток"), і причину ("total дорівнює 0"), і корисну деталь (part=5). Це саме та інформація, якої вам бракуватиме, коли програма «падає» в несподіваному місці, а ви намагаєтеся зрозуміти, де саме користувач увів нісенітницю.

Шпаргалка: errors.New vs fmt.Errorf

Щоб не гадати щоразу «яку кнопку натиснути», зручно тримати просте правило: якщо повідомлення фіксоване — errors.New, якщо в тексті потрібна підстановка — fmt.Errorf.

Ситуація Що обрати Чому
Помилка завжди одна й та сама («порожній ввід»)
errors.New("порожній ввід")
коротше, зрозуміліше, менше шуму
Треба вставити значення (число/рядок)
fmt.Errorf("некоректний вік: %d", age)
у помилці з’являється конкретика
Треба вставити рядок і побачити пробіли/порожнечу
fmt.Errorf("некоректне імʼя %q", name)
%q показує рядок «як є» у лапках
Є чужа помилка err, хочемо дописати до неї контекст текстом
fmt.Errorf("розбір ліміту: %v", err)
додали контекст, але без ланцюжка причин

5. Що повертати в (T, error) у разі помилки

Цей момент здається нудним, але згодом заощадить вам години. Коли функція повертає (T, error), у нас є домовленість: якщо err != nil, то значення T не можна вважати коректним. Щоб коду, який викликає функцію, не довелося гадати, прийнято повертати zero value для T.

Саме тому ви так часто побачите конструкції на кшталт return 0, err або return "", err або return nil, err. Це не тому, що програмісти Go — фанати нулів. Це тому, що нульове значення знижує шанс випадково продовжити обчислення «ніби все нормально».

Невелика таблиця для пам’яті:

Тип результату T Zero value (що повертаємо в разі помилки)
int
0
string
""
bool
false
[]int
nil
map[string]int
nil

Мініприклад із поверненням рядка:

package main

import (
	"errors"
	"fmt"
)

func NonEmpty(s string) (string, error) {
	if len(s) == 0 {
		return "", errors.New("порожній рядок")
	}
	return s, nil
}

func main() {
	v, err := NonEmpty("")
	fmt.Printf("v=%q err=%v\n", v, err) // v="" err=порожній рядок
}

Тут "" — не «правильне значення», а маркер: «не вийшло, результату немає».

6. Як додати контекст до чужої помилки (без %w)

Дуже частий сценарій: ви викликаєте strconv.Atoi, він повертає помилку, але вам хочеться, щоб на вищому рівні було зрозуміло, що саме ви парсили. У цій лекції ми робимо це найпростішим способом: створюємо нову помилку через fmt.Errorf і вставляємо початкову як %v.

Чому це нормально? Тому що fmt.Errorf без %w просто будує новий текст, і ви отримуєте зрозуміле повідомлення для людини. Це додає контекст, але не перетворює внутрішню помилку на частину вашого контракту.

Приклад із парсингом:

package main

import (
	"fmt"
	"strconv"
)

func ParseLimit(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("розбір ліміту %q: %v", s, err)
	}
	if n < 1 {
		return 0, fmt.Errorf("ліміт має бути >= 1, отримано %d", n)
	}
	return n, nil
}

func main() {}

Зверніть увагу на %q: він покаже рядок у лапках. Якщо користувач введе " " (два пробіли), ви це побачите, а не будете хвилин десять дивитися на екран і думати: «чому воно порожнє, якщо воно не порожнє?».

7. Приклад: калькулятор із валідацією

Тепер зберемо все в невеликий застосунок, який продовжує наш курс: читаємо введення, парсимо, рахуємо, друкуємо. У попередніх темах ви могли робити «калькулятор» просто в main. Тепер почнемо писати акуратніше: заведемо функції, які повертають (int, error) і створюють помилки через errors.New/fmt.Errorf.

І так: це той момент, коли ваш код стає трохи дорослішим. Він усе ще маленький, але вже не «простирадло на 200 рядків у main».

Функція parseInt: перетворення рядка на число зі зрозумілою помилкою

Почнемо з парсингу числа. Ми не хочемо, щоб main знав деталі strconv.Atoi. Нехай main просто скаже: «дай мені число, а якщо не вийде — поверни помилку».

package main

import (
	"fmt"
	"strconv"
)

func parseInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("розбір цілого числа %q: %v", s, err)
	}
	return n, nil
}

Тут ми використовуємо fmt.Errorf, тому що нам важливо показати проблемне введення %q і додати початкову помилку %v.

Функція compute: обчислюємо й перевіряємо операцію

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

package main

import (
	"errors"
	"fmt"
)

func compute(a, b int, op string) (int, error) {
	switch op {
	case "+":
		return a + b, nil
	case "/":
		if b == 0 {
			return 0, errors.New("ділення на нуль")
		}
		return a / b, nil
	default:
		return 0, fmt.Errorf("невідома операція %q", op)
	}
}

Зауважте, як тут розподілилися інструменти. «Ділення на нуль» — фіксована причина, отже errors.New. «Невідома операція» — залежить від op, отже fmt.Errorf.

Збираємо main: читаємо 3 токени й обробляємо помилки

Зберемо мінімальну програму, яка очікує введення виду: 10 / 2 або 7 + 5.

package main

import "fmt"

func main() {
	var aStr, op, bStr string
	fmt.Scan(&aStr, &op, &bStr)

	a, err := parseInt(aStr)
	if err != nil {
		fmt.Println("помилка:", err)
		return
	}

	b, err := parseInt(bStr)
	if err != nil {
		fmt.Println("помилка:", err)
		return
	}

	res, err := compute(a, b, op)
	if err != nil {
		fmt.Println("помилка:", err)
		return
	}

	fmt.Println("результат:", res)
}

Якщо запустити з введенням 10 / 0, отримаєте зрозумілу відмову. Якщо запустити з введенням 10 ^ 3, отримаєте невідома операція "^". І головне: програма не вдає, що все добре, коли насправді все погано.

Схема потоку помилок

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

flowchart TD
    A[Зчитати введення] --> B["parseInt(a)"]
    B -->|err!=nil| E[Надрукувати помилку; повернутися]
    B --> C["parseInt(b)"]
    C -->|err!=nil| E
    C --> D["compute(a,b,op)"]
    D -->|err!=nil| E
    D --> F[Надрукувати результат]

Це саме той стиль мислення, який робить Go-код спокійним: кожен крок або успішний, або завершує функцію з помилкою.

8. Типові помилки

Помилка № 1: повертати «майже нормальне значення» разом із помилкою і сподіватися, що його не використають.
Наприклад, повернути -1 або 999 «як ознаку» і помилку поруч. У реальності хтось обов’язково забуде перевірити err і почне рахувати далі. Краще повертати zero value (0, "", nil) — це знижує шанс, що результат виглядатиме «правдоподібно».

Помилка № 2: робити помилки надто загальними.
Повідомлення errors.New("некоректне введення") без контексту швидко перетворюється на «ну так, введення некоректне, а яке саме?». Якщо помилка пов’язана з конкретним значенням — використовуйте fmt.Errorf і додавайте проблемне введення через %q або %d. Це особливо важливо під час парсингу.

Помилка № 3: плутати ситуації для errors.New і fmt.Errorf та всюди писати fmt.Errorf «на автоматі».
fmt.Errorf("ділення на нуль") технічно працює, але створює враження, що тут десь має бути %d, просто забули. Якщо форматування не потрібне — errors.New читабельніший і чесніший.

Помилка № 4: склеювати рядки вручну замість fmt.Errorf.
Іноді новачки пишуть "погане введення: " + s, а потім дивуються, що складно додати число, лапки й різні формати. fmt.Errorf робить це акуратніше, а %q ще й допомагає побачити «невидимі» пробіли.

Помилка № 5: друкувати помилку всередині функції замість повернення помилки назовні.
Якщо parseInt почне саме друкувати fmt.Println("помилка"), ви втратите контроль: хто відповідає за виведення, хто — за формат і що робити далі. Набагато простіше: функція повертає error, а рішення «друкувати/не друкувати» приймає код, який її викликає (наприклад, main).

1
Задача
Go SELF, 16 рівень, 1 лекція
Недоступна
Ділення без аварії
Ділення без аварії
1
Задача
Go SELF, 16 рівень, 1 лекція
Недоступна
Паспортний вік
Паспортний вік
1
Задача
Go SELF, 16 рівень, 1 лекція
Недоступна
Ціна зі знижкою
Ціна зі знижкою
1
Задача
Go SELF, 16 рівень, 1 лекція
Недоступна
Калькулятор токенів
Калькулятор токенів
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ