JavaRush /Курси /Go SELF /errors.Is і errors.As

errors.Is і errors.As

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

1. Перевіряємо помилки за змістом, а не за err.Error()

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

У Go рядок err.Error() — це передусім повідомлення для людини, а не контракт для програми. Якщо ви починаєте ухвалювати рішення через strings.Contains(err.Error(), "..."), ви прирікаєте себе на вічні страждання: «Чому на одному комп’ютері все працює, а на іншому помилка інша?».

Іноді рядок усе ж друкують — у лог, користувачеві або в CLI, — але розгалуження логіки краще будувати не на рядку, а на перевірюваних сутностях. Для цього в Go і існують errors.Is та errors.As.

2. errors.Is: перевірка помилки на конкретну причину

errors.Is(err, target) відповідає на запитання: «Чи є err або будь-яка з причин у ланцюжку саме помилкою target?». Це важливо, бо після wrapping порівняння err == target майже завжди перестає працювати: назовні повертається нова помилка-обгортка, а не початкова.

Ключовий сенс у тому, що errors.Is сам проходить по ланцюжку причин, ніби ви вручну викликали errors.Unwrap у циклі, але без зайвої рутини та помилок.

Міні-демо: err == ErrX ламається після wrapping

Уявімо маркерну (sentinel) помилку — заздалегідь створене значення:

package main

import (
	"errors"
	"fmt"
)

var ErrBadInput = errors.New("неправильне введення")

func main() {
	err := fmt.Errorf("розбір: %w", ErrBadInput)

	fmt.Println(err == ErrBadInput)          // false
	fmt.Println(errors.Is(err, ErrBadInput)) // true
}

err == ErrBadInputfalse, тому що err тепер є «обгорткою». А от errors.Is(err, ErrBadInput) — true, бо errors.Is уміє зазирнути всередину.

Важливий наслідок: errors.Is працює тільки якщо причина справді «всередині»

Якщо ви додали помилку як текст через "%v", ланцюжка причин не буде, і errors.Is там нічого не знайде.

Ось чому "%w" — не «краса», а цілком практичний інструмент: він робить причину придатною для програмної перевірки.

3. errors.As: витягуємо помилку потрібного типу

Якщо errors.Is відповідає «так/ні: це та причина?», то errors.As відповідає на інше запитання: «Чи є всередині цієї помилки або в ланцюжку її причин помилка конкретного типу, і чи можна дістати її як значення, щоб подивитися деталі?».

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

Навіщо це потрібно на практиці

Типізована помилка особливо корисна, коли вам потрібні деталі, а не просто клас.

Наприклад, strconv.Atoi повертає помилку типу *strconv.NumError. У неї є поля, і інколи нам важливо зрозуміти, що саме не розпарсилося або яка операція провалилася.

Зробімо приклад: парсимо число, обгортаємо помилку контекстом через "%w", а потім витягуємо типізовану помилку:

package main

import (
	"errors"
	"fmt"
	"strconv"
)

func main() {
	_, err := strconv.Atoi("12x")
	err = fmt.Errorf("розбір числа: %w", err)

	var numErr *strconv.NumError
	if errors.As(err, &numErr) {
		fmt.Println("неправильне число:", numErr.Num) // неправильне число: 12x
	}
}

Ми не аналізуємо рядок помилки, не шукаємо «invalid syntax» і не ворожимо за фазами Місяця. Ми перевіряємо тип причини й дістаємо його як нормальний Go-об’єкт.

4. Чому errors.As приймає &target

Зазвичай у новачків виникає цілком чесне запитання: «Чому не можна просто errors.As(err, target)? Навіщо цей амперсанд, і чому інколи виходить „вказівник на вказівник“?».

errors.As має записати знайдену помилку у вашу змінну target. Щоб функція могла змінити змінну, їй потрібно дати адресу цієї змінної. Це та сама логіка, що й у fmt.Scan(&x): ви передаєте «куди покласти результат».

Коротка аналогія зі Scan

Із Scan ви вже стикалися:

package main

import "fmt"

func main() {
	var x int
	fmt.Scan(&x) // &x — куди записати прочитане число
}

errors.As працює за тим самим принципом, тільки «читає» вона не зі stdin, а з ланцюжка помилок.

Що означає *T у цільовому типі

У прикладах часто пишуть так:

var numErr *strconv.NumError
errors.As(err, &numErr)

Важливо прочитати це спокійно:

  • numErr — змінна, яка може зберігати значення типу «вказівник на strconv.NumError».
  • &numErr — адреса змінної numErr, щоб errors.As могла покласти туди знайдену помилку.
  • Так, візуально це виглядає як «вказівник на вказівник», і це нормально: просто помилка такого типу часто сама є вказівником.

Якщо тримати в голові фразу «&target — це „куди записати відповідь“», усе стає помітно простіше.

5. errors.Is vs errors.As: коли що обирати

З errors.As легко переборщити: «О, можна дістати тип — значить, буду діставати завжди». Але в більшості випадків вам не потрібні деталі конкретного типу помилки. Вам потрібно зрозуміти зміст ситуації: «неправильне введення», «не знайдено», «немає прав», «проблема введення/виведення». Для таких розвилок краще підходить errors.Is: він читається простіше і менше прив’язує ваш код до внутрішньої будови конкретної помилки.

errors.As варто брати тоді, коли без «внутрішностей» справді не обійтися: вам потрібно витягти поля типізованої помилки й на їхній основі сформувати поведінку або повідомлення. Наприклад, зрозуміти, яке саме значення не розпарсилося, який параметр був неправильним, який тип очікувався.

Нижче таблиця для швидкої навігації:

Запитання Інструмент Що ви отримуєте
«Це помилка саме цієї конкретної причини
errors.Is(err, target)
bool
«Усередині є помилка саме цього типу, і чи можна її дістати?»
errors.As(err, &target)
bool + заповнений target

Якщо зовсім коротко: errors.Is відповідає «це та причина (маркер) десь усередині?», а errors.As — «чи є помилка потрібного типу усередині, щоб дістати її деталі?».

6. Приклад: парсимо id і розгалужуємо обробку

Зробімо невеликий приклад. Нехай у нас є навчальний консольний застосунок (умовно назвемо його tasker), який приймає команду й аргументи. Ми поки що не будуємо повноцінний CLI на flag (це інша тема), але вже хочемо:

  • розпарсити id із рядка,
  • повернути помилку з контекстом,
  • у main зрозуміти, що це за помилка: «користувач увів не число» чи «щось інше».

Функція parseID: повертаємо контекст, але не втрачаємо причину

package main

import (
	"fmt"
	"strconv"
)

func parseID(s string) (int, error) {
	id, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("розбір id %q: %w", s, err)
	}
	return id, nil
}

Тут важливий стиль: контекст короткий і по суті. Ми не пишемо «ERROR!!!! parse id failed!!!», а описуємо операцію: "розбір id \"...\"". Для людини це вже корисно, а для коду причина err лишилася доступною через ланцюжок.

У main: витягуємо *strconv.NumError через errors.As

package main

import (
	"errors"
	"fmt"
	"strconv"
)

func main() {
	_, err := parseID("12x")
	if err != nil {
		var numErr *strconv.NumError
		if errors.As(err, &numErr) {
			fmt.Println("id має бути цілим числом") // id має бути цілим числом
			return
		}

		fmt.Println("неочікувана помилка:", err)
	}
}

Повідомлення користувачеві просте («id має бути цілим числом»), а «страшна» помилка з деталями лишається в err — ви можете її залогувати, вивести в налагодженні або показати в докладному режимі.

Альтернатива: errors.Is для «класу» помилки через sentinel

Іноді ми не хочемо залежати від конкретного типу помилки зі стандартної бібліотеки. Нам потрібне своє поняття: «неправильне введення». Тоді зручнішою стає зв’язка sentinel + errors.Is: errors.Is перевіряє, чи є в ланцюжку ось такий маркер.

Як читати чужий код: ручний Unwrap vs errors.Is/errors.As

Коли ви читатимете реальний Go-код (і особливо стандартну бібліотеку), ви зустрінете обидва підходи. Іноді люди вручну йдуть по ланцюжку через errors.Unwrap, але частіше — використовують errors.Is та errors.As, бо це коротше й менше схильне до помилок.

Щоб зрозуміти механіку, корисно уявити ланцюжок як «матрьошку»:

flowchart TD
    E0["err: «команда: розбір id 12x: ...»"] --> E1["обгортка → *strconv.NumError"]

errors.Is/errors.As роблять приблизно те саме, тільки без того, щоб ви писали цикл самі.

7. Типові помилки під час роботи з errors.Is і errors.As

Помилка №1: перевіряти помилки через err.Error() або strings.Contains.
Такий код спочатку здається простим, але він ламається від будь-якої зміни тексту, мови або версії залежності. Якщо вам потрібна перевірюваність — використовуйте errors.Is (для конкретної маркерної помилки) або errors.As (для конкретного типу помилки).

Помилка №2: після wrapping порівнювати err == target і дивуватися, що «не збіглося».
Після fmt.Errorf("ctx: %w", ErrX) назовні повертається вже не ErrX, а обгортка. Тому порівняння == перестає бути робочим інструментом. Правильна думка тут така: «Якщо є wrapping — перевірку причини робимо через errors.Is».

Помилка №3: забути, що errors.As приймає адресу змінної (&target).
Якщо передати не &target, а просто target, errors.As не зможе записати знайдену помилку, і код або не скомпілюється, або працюватиме неправильно за змістом. Тримайте в голові аналогію: це як Scan(&x) — «куди покласти результат».

Помилка №4: очікувати, що errors.Is/errors.As запрацюють, якщо ви «обгорнули» помилку через "%v".
fmt.Errorf("ctx: %v", err) додає текст, але не створює ланцюжка причин. Для errors.Is/errors.As це виглядає як звичайна нова помилка без першопричини всередині. Якщо причина має бути перевірюваною — обгортайте через "%w".

Помилка №5: застосовувати errors.As там, де достатньо errors.Is.
errors.As потрібен, коли вам справді важливі деталі конкретного типу. Якщо ви просто розгалужуєте поведінку («не знайдено» / «немає прав» / «неправильне введення»), типізована помилка часто буде зайвою прив’язкою до внутрішньої будови. У таких випадках простіше й стабільніше проєктувати розпізнавані причини та перевіряти їх через errors.Is.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ