Бібліотечні процедури часто повинні повертати користувачеві деяке повідомлення про помилку. Як згадувалося раніше, множинне повернення у Go дозволяє легко повертати детальний опис помилки разом зі звичайним значенням, що повертається. Хорошим стилем є використання цієї можливості для надання детальної інформації про помилку. Наприклад, як ми побачимо, os.Open
не просто повертає вказівник nil
у разі невдачі, але й значення помилки, яке описує, що саме пішло не так.
За домовленістю, помилки мають тип error
, простий вбудований інтерфейс.
type error interface {
Error() string
}
Автор бібліотеки може реалізувати цей інтерфейс з багатшою моделлю під кришкою, що дозволить не лише побачити помилку, але й надати певний контекст. Як уже згадувалося, окрім звичайного значення *os.File
, що повертається, os.Open
також повертає значення помилки. Якщо файл відкрито успішно, помилка буде nil
, але якщо виникла проблема, він буде містити os.PathError
:
// PathError фіксує помилку та операцію
// і шлях до файлу, який її спричинив.
type PathError struct {
Op string // "open", "unlink" тощо.
Path string // Асоційований файл.
Err error // Повернуто системним викликом.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
Функція Error
у PathError
генерує такий рядок:
open /etc/passwx: no such file or directory
Така помилка, яка містить ім'я проблемного файлу, операцію та помилку операційної системи, яку ця операція спричинила, є корисною, навіть якщо її виведено далеко від виклику, який її спричинив; вона є набагато інформативнішою, ніж просте «такого файлу або каталогу не існує».
Якщо це можливо, у рядках помилок слід вказувати їхнє походження, наприклад, за допомогою префікса, що позначає операцію або пакунок, який згенерував помилку. Наприклад, у пакеті image
рядок помилки декодування через невідомий формат має вигляд "image: unknown format"
.
Користувачі, яких цікавлять точні деталі помилки, можуть скористатися типізованим switch
або твердженням типу для пошуку конкретних помилок і вилучення деталей. У випадку з PathErrors
це може включати перевірку внутрішнього поля Err
на наявність помилок, які можна виправити.
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Звільнити трохи місця.
continue
}
return
}
Другий оператор if
тут є ще одним твердженням типу. Якщо він не спрацює, то ok
буде false
, а e
буде nil
. Якщо він спрацює, то ok
буде true
, що означає, що помилка була типу *os.PathError
, а також e
, який ми можемо дослідити для отримання додаткової інформації про помилку.
Звичайний спосіб повідомити користувача про помилку — повернути помилку як додаткове значення. Канонічний метод Read
є добре відомим прикладом; він повертає лічильник байт та повідомлення про помилку. Але що робити, якщо помилку неможливо виправити? Іноді програма просто не може продовжити роботу.
Для цього існує вбудована функція panic
, яка фактично створює помилку під час виконання, яка зупиняє програму (але про це в наступному розділі). Функція приймає єдиний аргумент довільного типу — найчастіше рядок, який буде виведено під час завершення роботи програми. Це також спосіб вказати, що сталося щось неможливе, наприклад, вихід з нескінченного циклу.
// Реалізація обчислення кубічного кореня за методом Ньютона.
func CubeRoot(x float64) float64 {
z := x / 3 // Довільне початкове значення
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z - x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// Мільйон ітерацій не зійшовся; щось не так.
panic(fmt.Sprintf("CubeRoot(%g) не зійшовся", x))
}
Це лише приклад, але у реальних бібліотечних функціях слід уникати паніки. Якщо проблему можна замаскувати або обійти, завжди краще не зупиняти роботу, аніж виводити з ладу всю програму. Один з можливих контрприкладів — під час ініціалізації: якщо бібліотека дійсно не може налаштувати себе, може бути розумним, так би мовити, панікувати.
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
Коли викликається panic
, у тому числі неявно при помилках під час виконання, таких як індексація фрагмента за межами допустимих значень або помилка перевірки типу, вона негайно зупиняє виконання поточної функції й починає розгортати стек горутини, виконуючи всі відкладені функції по дорозі. Якщо це розгортання досягає вершини стека горутини, програма завершується. Однак, можна скористатися вбудованою функцією recover
, щоб повернути контроль над горутиною й відновити нормальне виконання.
Виклик recover
зупиняє розгортання і повертає аргумент, переданий у panic
. Оскільки єдиний код, який виконується під час розгортання, знаходиться всередині відкладених функцій, recover
корисна лише всередині відкладених функцій.
Одне із застосувань recover
— це зупинка горутини, що вийшла з ладу на сервері, не вбиваючи при цьому інші горутини, що виконуються.
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) { } }
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
У цьому прикладі, якщо do(work)
запанікує, результат буде залоговано, і горутина завершить роботу, не заважаючи іншим. У відкладеному закритті не потрібно робити нічого іншого; виклик recover
повністю обробляє умову.
Оскільки recover
завжди повертає nil
, якщо не викликається безпосередньо з відкладеної функції, відкладений код може без проблем викликати бібліотечні процедури, які самі використовують panic
і recover
. Наприклад, відкладена функція у safelyDo
може викликати функцію логування перед викликом recover
, і код логування працюватиме без впливу стану паніки.
З нашим шаблоном відновлення функція do
(і все, що вона викликає) може вийти з будь-якої поганої ситуації, просто викликавши panic
. Ми можемо використовувати цю ідею для спрощення обробки помилок у складному програмному забезпеченні. Розглянемо ідеалізовану версію пакета regexp
, який повідомляє про помилки синтаксичного аналізу шляхом виклику panic
з локальним типом помилки. Ось визначення Error
, методу error
та функції Compile
.
// Error - це тип помилки розбору; він задовольняє інтерфейс error.
type Error string
func (e Error) Error() string {
return string(e)
}
// error - метод *Regexp, що повідомляє про помилки розбору,
// викликаючи паніку за допомогою Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile повертає розібране представлення регулярного виразу.
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse викличе паніку, якщо станеться помилка розбору.
defer func() {
if e := recover(); e != nil {
regexp = nil // Очистити значення, що повертається.
err = e.(Error) // Знову викличе паніку, якщо це не помилка розбору.
}
}()
return regexp.doParse(str), nil
}
Якщо doParse
панікує, блок відновлення встановить nil
значенням повернення — відкладені функції можуть змінювати іменовані значення повернення. Потім він перевірить у присвоєнні err
, що проблема була помилкою синтаксичного аналізу, стверджуючи, що вона має локальний тип Error
. Якщо це не так, твердження типу не спрацює, що призведе до помилки під час виконання, яка продовжить розгортання стека так, ніби його ніщо не переривало. Ця перевірка означає, що якщо трапиться щось непередбачуване, наприклад, індекс вийде за межі, код завершиться невдачею, навіть якщо ми використовуємо паніку та відновлення для обробки помилок синтаксичного аналізу.
Завдяки обробці помилок, метод error
(оскільки це метод, прив'язаний до типу, цілком нормально, навіть природно, що він має те саме ім'я, що і вбудований тип error
) дозволяє легко повідомляти про помилки розбору, не турбуючись про розмотування стека розбору власноруч:
if pos == 0 {
re.error("'*' illegal at start of expression")
}
Хоч цей патерн і корисний, його слід використовувати лише всередині пакета. Parse
перетворює свої внутрішні виклики panic
у значення error
; він не показує panic
своєму клієнту. Це хороше правило, якого слід дотримуватися.
До речі, ця ідіома повторної паніки змінює значення паніки, якщо виникає справжня помилка. Однак, як початкова, так і нова помилки будуть представлені у звіті про збої, тому першопричину проблеми все одно буде видно. Таким чином, цього простого підходу до повторної паніки зазвичай достатньо — це все ж таки збій — але якщо ви хочете відображати лише початкове значення, ви можете написати трохи більше коду для фільтрації неочікуваних проблем і повторної паніки з початковою помилкою. Залишаємо це як вправу для читача.