Обробка помилок у Golang — усе, що ви не знали або боялися спитати
Нещодавня стаття породила багато старого доброго хаосу в коментарях. Безліч думок висловлено і кінця не видно.
Взагалі суперечки «о мовах» це скоріш релігійні дебати, але ця стаття з’явилася за для того, щоб відповісти на головне «питання» що до обробки помилок в Golang. На момент написання статті останньою версією Go вважається 1.24.
Вступ
У мові Go робота з помилками (error handling) зроблена подібно до того, як і в мові з якої вона виросла — С. Але на відміну від «батьківської мови» в Go помилка це конкретний інтерфейс з яким взаємодіють усі модулі, зокрема й ті, що спеціалізовані для роботи с помилками.
Важливо пам’ятати — помилка як тип в Golang це реалізація інтерфейсу. Обов’язкове поле в помилці це рядок з текстом помилки (інтерфейс має лише метод Error() string), але це не означає що помилка може складатися з одних тільки рядків.
Для тих хто не знає, в Go інтерфейс як тип що обробляється — це завжди вказівник. Тобто Ви можете реалізувати власний тип помилки з купою полів різних типів і він працюватиме так само як і «стандартна помилка» створена за допомогою errors.New().
Але і це ще не все, окрім широких можливостей кастомізації помилок, не порушуючи строгу типізацію, мова надає додаткові інструменти роботи з помилками. У випадку коли потрібно обробляти надзвичайне завершення контрольовано в Golang завжди була можливість перехоплювати паніку. А починаючи з версії 1.13 в Go з’явилися вбудовані функції для зручнішої обробки помилок: errors.Is, errors.As, errors.Unwrap. Не так давно (1.20) до них додали errors.Join.
Далі я детально і з прикладами про це розповім.
Боротьба з ланцюгами err != nil
Сама основна «проблема» в мові, на яку вказують інші це на їх думку обов’язковість і нескінченість err != nil.
В цілому якщо у вас в коді, в конкретному методі виникають великі ланцюги err != nil то це означає, що щось ви робите не так. Але іноді такі сценарії потрібні і зайвий копіпаст вирішується конструкцією recover() із «завершенням» через panic().
Приклад:
Таким чином ми можемо викликом panic() завершувати виконання, прописавши дії до завершення з помилкою один раз, а не кожен раз через конструкцію if.
Але я нагадую, що це боротьба з наслідками (або намагання притягнути свої звички з іншої мови в Golang) і зазвичай, якщо у Вас в одному методі не один десяток err != nil то скоріш за все ви робите щось не так (наприклад обробка err коли передаються гарантовано валідні дані для hex.DecodeString())
Пошук та порівняння помилок за допомогою errors.Is()
Функція errors.Is() відповідає на запитання: «Чи дорівнює ця помилка такій-то іншій помилці (або чи містить її в ланцюжку)?» Це дозволяє перевіряти типи чи значення помилок без необхідності самостійно переглядати кожну ланку ланцюга.
Приклад:
Функція errors.Is() сама розгорне err2 та побачить, що всередині лежить err1.
Це також працює і з ланцюжками зібраними за допомогою errors.Join() — якщо в ньому е хоча б одна така помилка то він поверне true.
«Пакування» та «розпаковування» помилок за допомогою errors.Join() та errors.Unwrap()
Функція errors.Join() дає змогу об’єднати кілька помилок в одну «комбіновану» помилку. Важливо пам’ятати що на момент 1.24 повноцінно працює з таким масивом помилок лише errors.Is() та errors.As(). Метод errors.Unwrap() завжди поверне nil якщо намагатися «розпакувати» помилку зібрану errors.Join().
Функція errors.Unwrap() дістає внутрішню (запаковану) помилку, якщо вона є. Вона працює, якщо помилка була створена чи обгорнута функцією fmt.Errorf("%w", err), або реалізований тип помилки має метод Unwrap() error.
Важливо пам’ятати що використовуючи fmt.Errorf можна «вкласти» лише одну помилку (по суті обгорнути помилку в текстовий коментар), якщо намагатися додати таким чином дві та більше, то метод errors.Unwrap() завжди буде повертати nil.
Приклад:
Метод errors.Is() знайшов запаковану помилку, за допомогою errors.Join(), але при цьому намагання «розпакувати» не пусту таку помилку буде nil. Але «розпакування» працює як зі стандартним fmt.Errorf, так із власним типом помилки.
Приклад 2:
А в цьому прикладі власна реалізація інтерфейсу error має підтримку вкладених помилок, щоб їх можна було знаходити за допомогою errors.Is() та errors.As().
Трансформація помилки в конкретний тип за допомогою errors.As()
Функція errors.As() дозволяє віднести помилку до конкретного типу і «трансформувати» її в нього. Це зручно, коли ви визначаєте власний тип помилок (наприклад, структуру, що реалізує інтерфейс error) і хочете дістати доступ до її полів або методів.
5 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівЯ б доповнив ще списком корисних ресурсів:
Щось errors.Unwrap не те unwrap-ає — вертає nil
go.dev/play/p/OQnvpDLiFcR
Вау, дійсно. Виправлю статтю, дякую за інфоримацію.
Доречі про це написано в документаціі, правда не до 1.20 де додали, але вже с 1.21 пишуть що не буде працювати з Join pkg.go.dev/[email protected]#Unwrap
І все, тому що розгортання реалізується через інтерфейс `Unwrap() error` а Join має лише `Unwrap() []error`
Взагалі мені захотілося написати окремим топіком як раз із-за того що загуглив конкретику, дізнався що з’явився новий errors.Join і в кращих традиціях хабра — ніщо так не розвиває кругозор як написання статті по темі.
UPD
errors.Join() розрахован на роботу з Is/As судячи з коду https://cs.opensource.google/go/go/+/refs/tags/go1.24.2:src/errors/wrap.go;drc=85115da5d3351c4300337579eafe726700f3b587;l=67
статтю виправив
Дякую за статтю.
Додайте підсвітку коду — це просто.
А ще, якщо ви описуєте, як щось працює, то підкріпіть це тестами.
Дякую, як раз взагалі не зрозумів як працюе підсвітка коду, зроблю.
І що ви маете на у вазі кажучі за тести? Всі приклади коду в статті робочі
UPD
А ні, доу без сюрпризів, розмітка вказана в статті не працюе.
Може це для якогость альтернативного доу тому як у мене редактор явно з md зв’язан.
І взагалі нема можливості переключити редактор в формат коду щоб бачити стилі тегами
UPD-2
а використовування gist.github спрацювало