В Go ошибка это обычное значение. Функция возвращает её последним результатом, вызывающий проверяет if err != nil. Исключений нет, а panic предназначен для действительно невозможных ситуаций.
Интерфейс error
type error interface { Error() string }
Создать ошибку: errors.New("not found") или fmt.Errorf("load user %d: %w", id, err).
Обёртка и цепочки
Глагол %w в fmt.Errorf оборачивает ошибку: снаружи добавляется контекст, внутри сохраняется исходная. Достают её функции пакета errors:
var ErrNotFound = errors.New("not found") // sentinel-ошибка
err := fmt.Errorf("get user: %w", ErrNotFound)
errors.Is(err, ErrNotFound) // true: ищет по всей цепочке
err == ErrNotFound // false: это уже другая ошибка
var pe *fs.PathError
if errors.As(err, &pe) { fmt.Println(pe.Path) } // достаёт ошибку нужного типа
%v вместо %w превращает ошибку в текст, и errors.Is её уже не найдёт. С Go 1.20 в одном Errorf можно несколько %w, а errors.Join собирает несколько ошибок в одну.
Свои типы ошибок
type ValidationError struct{ Field string }
func (e *ValidationError) Error() string { return "invalid " + e.Field }
Sentinel-ошибки (io.EOF) для простых случаев, свой тип, когда нужны поля. Обёртку отдавайте наружу, только если готовы поддерживать её как часть API.
defer
defer откладывает вызов до выхода из функции. Отложенные вызовы выполняются в обратном порядке (стек). Аргументы вычисляются сразу в момент defer, а не при выполнении:
x := 1
defer fmt.Println(x) // напечатает 1
x = 2
Отложенная функция может читать и менять именованные результаты:
func f() (n int) {
defer func() { n *= 2 }()
return 3 // n = 3, затем defer, итог 6
}
return 3 сначала присваивает результат, потом выполняются defer, потом функция выходит. Так делают defer с проверкой ошибки Close при записи файла.
defer в цикле копит вызовы до выхода из функции, а не из итерации: файлы в цикле лучше закрывать во вложенной функции.
panic и recover
panic разматывает стек горутины, выполняя её defer. Если никто не перехватил, программа падает с кодом 2 и печатает стек. Перехватить можно только recover(), вызванным прямо в отложенной функции:
func safe() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
panic("boom")
}
Правила:
recoverработает только внутриdefer-функции этой же горутины. Panic в другой горутине так не поймать, она уронит весь процесс.recoverвне паники возвращает nil.- Типичный случай: middleware HTTP-сервера, чтобы один запрос не убил сервер.
net/httpсам ловит панику в обработчике и закрывает соединение, но логировать лучше самим. - Не используйте panic для обычных ошибок: неверный ввод, нет файла, таймаут.
Некоторые сбои нельзя перехватить вовсе: fatal error: concurrent map writes, нехватка памяти, deadlock всех горутин.
Частые ошибки
- Игнорировать ошибку через
_. - Логировать и возвращать одну ошибку на каждом уровне: дубли в логах.
- Сравнивать обёрнутую ошибку через
==. - Возвращать типизированный nil-указатель как
error. - Ждать, что
recoverпоймает панику из другой горутины.
Что спросят на собесе
- Почему в Go ошибки значения, а не исключения.
- Чем
errors.Isотличается отerrors.As, зачем%w. - В каком порядке выполняются defer и когда вычисляются аргументы.
- Как defer меняет именованный результат.
- Как работает recover и почему он не ловит панику соседней горутины.