Обработка ошибок в Go
| Принцип | ошибки — значения, возвращаемые явно |
|---|---|
| Тип | встроенный интерфейс error с методом Error() string |
| Обёртывание | fmt.Errorf с %w, errors.Is и errors.As (Go 1.13, 2019) |
| Несколько ошибок | errors.Join (Go 1.20) |
| Исключительные ситуации | panic и recover |
| Синтаксический сахар | предложения отклонены (2019, 2025) |
Обработка ошибок в Go построена не на исключениях, а на явных возвращаемых значениях. Функция, которая может завершиться неудачей, возвращает последним результатом значение встроенного типа error; вызывающий код обязан его проверить. Отсюда знаменитая конструкция if err != nil, которая встречается в Go-коде повсюду. Роб Пайк сформулировал философию так: «ошибки — это значения», и с ними можно программировать, как с любыми другими данными.[5]
Подход делает пути ошибок видимыми в коде: читая функцию, сразу видно, где что-то может пойти не так и что тогда произойдёт.
Тип error
type error interface {
Error() string
}
Любой тип с методом Error() string является ошибкой. Типичная функция возвращает результат и ошибку:
func ReadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("чтение конфигурации %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("разбор конфигурации %s: %w", path, err)
}
return &cfg, nil
}
func main() {
cfg, err := ReadConfig("app.json")
if err != nil {
log.Fatal(err) // чтение конфигурации app.json: open app.json: no such file or directory
}
_ = cfg
}
По соглашению при ненулевой ошибке остальные возвращаемые значения не используются, а сообщения об ошибках пишут со строчной буквы и без точки в конце, потому что их часто вкладывают друг в друга.[4]
Обёртывание и анализ
Go 1.13 (2019) добавил стандартный механизм обёртывания: глагол %w в fmt.Errorf сохраняет исходную ошибку внутри новой, добавляя контекст. Функции пакета errors разворачивают цепочку:[2]
var ErrNotFound = errors.New("не найдено") // ошибка-маркер (sentinel)
type ValidationError struct { // собственный тип ошибки с данными
Field string
}
func (e *ValidationError) Error() string { return "некорректное поле " + e.Field }
err := service.GetUser(ctx, id)
switch {
case errors.Is(err, ErrNotFound): // есть ли в цепочке конкретная ошибка
http.Error(w, "нет такого пользователя", http.StatusNotFound)
case errors.Is(err, context.DeadlineExceeded):
http.Error(w, "таймаут", http.StatusGatewayTimeout)
default:
var ve *ValidationError
if errors.As(err, &ve) { // есть ли ошибка нужного типа — и достать её
http.Error(w, ve.Error(), http.StatusBadRequest)
return
}
http.Error(w, "внутренняя ошибка", http.StatusInternalServerError)
}
errors.Isсравнивает с конкретным значением — для ошибок-маркеров вродеio.EOFилиsql.ErrNoRows.errors.Asищет ошибку определённого типа и даёт доступ к её полям.errors.Join(Go 1.20) объединяет несколько ошибок в одну — например, при закрытии нескольких ресурсов.[3]
Сравнивать ошибки по тексту сообщения (err.Error() == "...") не следует — текст может измениться.
panic и recover
panic предназначен для действительно исключительных ситуаций — ошибок программиста и нарушенных инвариантов: выход за границы массива, разыменование nil, невозможное состояние. Паника раскручивает стек, выполняя отложенные (defer) вызовы, и завершает программу, если её не перехватить через recover в отложенной функции. HTTP-серверы перехватывают панику в обработчике, чтобы один сбойный запрос не уронил весь сервис. Для ожидаемых ситуаций — файла нет, сеть недоступна, данные некорректны — используют обычные ошибки, а не панику.[1]
Критика и попытки упростить
Главная претензия к подходу — многословие: в типичной функции значительная часть строк приходится на if err != nil { return ..., err }. Команда Go несколько раз предлагала сократить запись — встроенная функция try (2019), оператор ? по образцу Rust, — но предложения не нашли поддержки сообщества. В 2025 году команда объявила, что больше не планирует менять синтаксис обработки ошибок, сосредоточившись на инструментах и библиотеках.[6]
Сторонники подхода отмечают, что явность — достоинство: ошибка не может «пролететь» через несколько уровней незамеченной, как необработанное исключение, а контекст, добавляемый на каждом уровне, даёт понятные сообщения вроде «сохранение заказа 42: запись в базу: connection refused».
Практические советы
- Добавляйте контекст при передаче ошибки наверх: что делала функция и с какими данными.
- Обрабатывайте ошибку один раз: либо залогируйте, либо верните наверх — не то и другое сразу, иначе одна ошибка появится в логах многократно.
- Не игнорируйте ошибки через
_без веской причины; линтеры (errcheck,go vetи сборки вроде golangci-lint) находят пропущенные проверки. - Экспортируйте ошибки-маркеры и типы в библиотеках, чтобы пользователи могли их различать, а внутренние детали оборачивайте.
- Учитывайте ошибки отмены и таймаута контекста — в сетевом коде с горутинами они встречаются постоянно.
Сравнение с другими языками
| Язык | Механизм |
|---|---|
| Go | значение error, явная проверка |
| Rust | тип Result<T, E>, компилятор требует обработать, оператор ? для проброса |
| Python, Java | исключения, try/except |
| C | коды возврата и errno |
См. также
- Go (язык)
- Обработка ошибок в Rust (Result, ?)
- Горутины и каналы
- Go-модули (go.mod)
- Тестирование в Go