- Писать функции, возвращающие ошибки, и проверять их через
if err != nil - Оборачивать ошибки через
%wи проверять их с помощьюerrors.Isиerrors.As - Объяснять, когда используются
panicиrecover
Файла может не оказаться, интернет может пропасть, пользователь может ввести буквы вместо числа. Многие языки в таких случаях выбрасывают исключение и ловят его через try/catch. Go выбрал другой путь: ошибка — это обычное значение, которое возвращает функция. Код становится чуть длиннее, зато ты ясно видишь, где может возникнуть каждая ошибка и как она обрабатывается.
Ошибка — обычное значение
В Go error — простой интерфейс: любой тип с методом Error() string является ошибкой. Правило такое: функция, которая может завершиться неудачей, возвращает ошибку последним результатом, а при успехе — nil. Вызывающий код сразу проверяет: if err != nil { ... }. Простейшую ошибку создаёт errors.New("текст").
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 4)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("result:", result)
if _, err := divide(1, 0); err != nil {
fmt.Println("error:", err)
}
}result: 2.5 error: division by zero
Обрати внимание: при ошибке в качестве результата возвращается 0, но вызывающий код на него не смотрит — сначала проверяется err. Основной код остаётся у левого края, а ошибка обрабатывается сразу, с быстрым выходом.
Обёртывание ошибок: %w и errors.Is
Когда ошибка передаётся наверх, полезно добавить контекст: чем мы занимались, когда она возникла? fmt.Errorf("load profile %d: %w", id, err) создаёт новую ошибку и оборачивает (wrap) в неё исходную. Спецификатор %w важен: именно он сохраняет исходную ошибку. Затем errors.Is(err, ErrNotFound) проходит всю цепочку и сообщает, есть ли внутри искомая ошибка. Заранее объявленные ошибки такого рода называют сигнальными (sentinel errors), а их имена обычно начинаются с Err.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func findStudent(id int) (string, error) {
students := map[int]string{1: "Aysel", 2: "Murad"}
name, ok := students[id]
if !ok {
return "", ErrNotFound
}
return name, nil
}
func loadProfile(id int) (string, error) {
name, err := findStudent(id)
if err != nil {
return "", fmt.Errorf("load profile %d: %w", id, err)
}
return "Profile of " + name, nil
}
func main() {
p, err := loadProfile(2)
fmt.Println(p, err)
_, err = loadProfile(7)
fmt.Println(err)
fmt.Println(errors.Is(err, ErrNotFound), err == ErrNotFound)
}Profile of Murad <nil> load profile 7: not found true false
Посмотри на последнюю строку: обёрнутая ошибка уже не равна ErrNotFound (== даёт false), но errors.Is находит её внутри цепочки. Поэтому ошибки всегда проверяй через errors.Is.
if err != nil && err.Error() == "not found" {
fmt.Println("no such student")
}if errors.Is(err, ErrNotFound) {
fmt.Println("no such student")
}load profile 7: not found), и первый вариант перестаёт работать. А errors.Is распознаёт и обёрнутые ошибки.Свои типы ошибок и errors.As
Иногда об ошибке нужно знать больше, чем её текст: какое поле неверно, какое значение пришло? Тогда создай собственный тип ошибки — структуру с методом Error() string. Чтобы достать такую ошибку из цепочки обёрток, используют errors.As: если в цепочке найдётся ошибка подходящего типа, функция запишет её в твою переменную и вернёт true.
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Value int
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid %s: %d", e.Field, e.Value)
}
func setAge(age int) error {
if age < 0 || age > 120 {
return &ValidationError{Field: "age", Value: age}
}
return nil
}
func main() {
err := fmt.Errorf("register user: %w", setAge(-3))
fmt.Println(err)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("field:", ve.Field, "value:", ve.Value)
}
fmt.Println(setAge(30) == nil)
}register user: invalid age: -3 field: age value: -3 true
| Инструмент | Что делает |
|---|---|
errors.New("...") | создаёт простую ошибку |
fmt.Errorf("ctx: %w", err) | добавляет контекст и оборачивает ошибку |
errors.Is(err, ErrX) | есть ли в цепочке конкретная ошибка? |
errors.As(err, &target) | достаёт из цепочки ошибку заданного типа |
panic и recover
panic — аварийная ситуация, которая прерывает нормальную работу программы: выход за границы массива, nil-указатель, целочисленное деление на ноль. Во время паники функции покидаются одна за другой, но их отложенные (defer) вызовы выполняются. Если отложенная функция вызовет recover(), паника прекращается, и программа продолжает работу.
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
result = a / b
return result, nil
}
func main() {
fmt.Println(safeDivide(10, 2))
fmt.Println(safeDivide(1, 0))
fmt.Println("program continues")
}5 <nil> 0 recovered: runtime error: integer divide by zero program continues
safeDivide(1, 0) вызвала панику, но отложенная функция перехватила её через recover и записала обычную ошибку в именованный результат err. Именованный результат здесь важен: именно благодаря ему отложенная функция может изменить возвращаемое значение.
Главное
- Ошибка — значение с методом
Error() string; функция возвращает её последним результатом, а при успехе —nil. - Сразу проверяй
if err != nilи передавай ошибку наверх с контекстом:fmt.Errorf("...: %w", err). - Обёрнутые ошибки проверяй через
errors.Is(конкретная ошибка) иerrors.As(тип ошибки), а не через==или сравнение текста. - Собственный тип ошибки может нести дополнительные сведения, например поле и значение.
panic— только для аварийных ситуаций;recoverможет перехватить её лишь внутри отложенной функции.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.