Обработка ошибок в Go

Материал из llmdocs — справочник разработчика
Портал: Go и Rust
Обработка ошибок в 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

См. также

Источники

  1. The Go Blog — Error handling and Go
  2. The Go Blog — Working with Errors in Go 1.13
  3. Go — package errors
  4. Effective Go — Errors
  5. Rob Pike — Errors are values (2015)
  6. The Go Blog — On | No syntactic support for error handling (2025)