Ошибки: error, defer, panic, обёртывание, Is/As/Join
В Go нет исключений. Ошибки это значения, которые ты возвращаешь и
обрабатываешь явно. Многословно? Да. Но взамен ты видишь все возможные
сбои в коде глазами, без скрытого try-graf-а, без перехвата на 5
уровней выше.
Этот урок собирает все техники работы с ошибками в одну картину: что
такое error как тип, как делать свои ошибки, как defer помогает
закрывать ресурсы, что такое обёртывание %w, как errors.Is/As
ищут конкретную ошибку в цепочке, и зачем errors.Join появился в Go
1.20.
После этого урока пойдёшь в 12-generics-basics, последний “технический” урок junior-трека.
1. error это интерфейс
type error interface {
Error() string
}
Один метод, одна строка. Любой тип с Error() string это error.
Создавать ошибки можно тремя способами:
import "errors"
import "fmt"
err1 := errors.New("not found") // самая простая
err2 := fmt.Errorf("user %d not found", id) // с форматированием
err3 := fmt.Errorf("get user: %w", err1) // с обёртыванием (см. §6)
errors.New для статичной строки. fmt.Errorf когда хочешь
подставить значения (или обернуть другую ошибку через %w).
Главная идиома: проверка после вызова
u, err := getUser(id)
if err != nil {
return fmt.Errorf("update name: %w", err) // оборачиваем и пробрасываем выше
}
// дальше работаем с u
90% Go-кода выглядит как if err != nil { ... }. Это нормально, не
борись с этим. Это видимая работа с ошибками, в обмен ты не
ловишь сюрпризы из глубоких стеков.
2. Кастомные типы ошибок
Когда хочется не строку, а структурированную ошибку (с полями, с методами), делают свой тип:
type ValidationError struct {
Field string
Reason string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s %s", e.Field, e.Reason)
}
func ValidateAge(n int) error {
if n < 0 {
return &ValidationError{Field: "age", Reason: "negative"}
}
if n > 150 {
return &ValidationError{Field: "age", Reason: "too large"}
}
return nil
}
Заметь:
- Метод
Errorна pointer receiver, по convention в Go-коде. Так пара(*ValidationError)(nil)распознаётся как nil-error (см. §3), и сравнениеerrors.As(err, &target)работает. - Возврат
&ValidationError{...}, неValidationError{...}. Указатель, иначе value receiver требовался бы. - Возврат
nilliteral, неvar e *ValidationError; return e, иначе typed-nil ловушка из 10-interfaces.
Sentinel-ошибки
Если хочется одной “канонической” ошибки на весь пакет, объявляют sentinel на уровне пакета:
var ErrNotFound = errors.New("not found")
func GetUser(id int) (User, error) {
// ...
return User{}, ErrNotFound
}
Снаружи это сравнивают через errors.Is(err, ErrNotFound) (см. §7).
В stdlib примеры: io.EOF, sql.ErrNoRows, os.ErrNotExist.
3. Когда ничего не возвращать: nil как идиома
func foo() error {
if все_хорошо {
return nil // настоящий nil-интерфейс
}
return errors.New("плохо")
}
Возвращать nil literal, не переменную с nil-указателем (см. урок 10
§8). Иначе err != nil сработает там, где не должен.
4. defer: LIFO и закрытие ресурсов
defer это отложенный вызов: функция, помеченная defer, вызовется
в момент выхода из текущей функции, что бы ни случилось (return,
panic, любой путь).
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // выполнится перед return, даже если io.ReadAll упадёт
return io.ReadAll(f)
}
Это ровно то, что нужно для закрытия ресурсов: файлов, соединений, mutex-ов. Не забудешь, не пропустишь, потому что defer срабатывает автоматически.
Три правила defer
-
LIFO-порядок. Несколько defer выполняются в обратном порядке от объявления:
defer fmt.Println("1") defer fmt.Println("2") defer fmt.Println("3") // напечатает 3, 2, 1 -
Аргументы вычисляются в момент defer, не в момент выполнения:
x := 1 defer fmt.Println("deferred:", x) // вычислится: "deferred: 1" x = 99 // печатает: deferred: 1Если хочешь, чтобы захватилось текущее, оборачивай в анонимную функцию:
defer func() { fmt.Println("deferred:", x) }() // 99 -
defer работает только в текущей функции, не в цикле всего блока. Это главная ловушка:
for _, path := range files { f, _ := os.Open(path) defer f.Close() // ← все Close выполнятся в конце функции, не итерации }Накопится N открытых файлов до конца. Лечение, выносить в отдельную функцию:
for _, path := range files { processFile(path) // defer внутри, в конце processFile }
defer и err через named return
Defer может поправить возвращаемое значение, если оно named:
func ReadConfig(path string) (cfg Config, err error) {
f, err := os.Open(path)
if err != nil {
return
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
// парсинг...
return cfg, nil
}
Замысел: если Close() вернул ошибку, и до этого ошибки не было,
вернуть Close-ошибку. Если уже была ошибка чтения, оставить её. Это
идиома “не маскируй первую ошибку второй”.
5. panic и recover: только для unrecoverable
panic это аварийный механизм. Программа печатает стек и падает:
panic("something is very wrong")
Когда паниковать обязательно:
- Программная инвариантность нарушена. “Этот if никогда не должен срабатывать, если он сработал, мы не знаем что делать.”
- Init-этап. “Не смогли загрузить конфиг, продолжать бессмысленно.”
- stdlib делает это сама. Деление на 0, выход за границы массива, обращение по nil-указателю.
Когда нельзя:
- Бизнес-логика. “Юзер не найден”, это
error, не паника. - Невалидный ввод. Возвращай
error. - Network-ошибка. Тем более.
recover
Чтобы поймать панику в текущей горутине, есть recover. Работает
только в defer:
func safeDo(work func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panicked: %v", r)
}
}()
work()
return nil
}
Шаблон с recover можно увидеть в HTTP-middleware: ловят панику в обработчике, чтобы не упал весь сервер. Подробно в 20-http-services.
recover в горутине
recover ловит только панику той горутины, в которой стоит. Если
дочерняя горутина паникует, родительская не узнает:
go func() {
panic("oops") // упадёт вся программа
}()
Каждая горутина обязана защитить себя, если есть риск паники. Подробно в 17-concurrency-deep.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.