Ошибки в Go: что нужно знать заранее
Go обрабатывает ошибки без try-catch — через возврат значения error из каждой функции. Разберём, как устроена эта модель, как создавать, оборачивать и проверять ошибки, и к чему готовиться до первого проекта.

Зачем отдельная модель ошибок
В большинстве языков ошибка — это исключение, которое летит вверх по стеку, пока кто-нибудь его не поймает. В Go исключений нет. Функция, которая может сломаться, возвращает два значения: результат и error. Вызывающий код обязан проверить error явно — иначе компилятор не ругнётся, но баг затаится до прода. Этот подход делает поток управления линейным и предсказуемым, но требует дисциплины: вы будете писать if err != nil десятки раз в день.
Что такое error
error — это интерфейс стандартной библиотеки с единственным методом:
// Go 1.21+
type error interface {
Error() string
}
Любой тип, у которого есть метод Error() string, реализует этот интерфейс. Строка, структура с десятью полями, обёртка над другой ошибкой — всё считается ошибкой, если удовлетворяет интерфейсу.
Нулевое значение интерфейса — nil. Если функция вернула nil вторым значением, всё хорошо. Если не nil — нужно разбираться.
Простейший способ создать ошибку
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("деление на ноль")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("ошибка:", err)
return
}
fmt.Println("результат:", result)
}
Функция errors.New из стандартной библиотеки принимает строку и возвращает значение, реализующее error. Для ошибок с подставляемыми данными удобнее fmt.Errorf:
return 0, fmt.Errorf("не удалось разделить %.2f на %.2f: делитель равен нулю", a, b)
if err != nil — привыкайте
Новички из мира Python или Java жалуются, что код на Go на 40 % состоит из проверки ошибок. Это не преувеличение — и это осознанный дизайн. Авторы языка считают, что явная обработка лучше скрытого потока исключений, который сложно отследить в большой кодовой базе.
Типичный паттерн:
data, err := os.ReadFile("config.yaml")
if err != nil {
return fmt.Errorf("чтение конфига: %w", err)
}
// дальше работаем с data
Обратите внимание: ошибка не глотается и не логируется на месте — она возвращается наверх с дополнительным контекстом. Это ключевой приём, и о нём ниже.
Оборачивание ошибок: глагол %w
Начиная с Go 1.13 в fmt.Errorf появился глагол %w (от wrap). Он создаёт новую ошибку, которая сохраняет ссылку на оригинальную. Это позволяет добавлять контекст на каждом уровне вызова, не теряя исходную причину.
func loadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
// оборачиваем: добавляем «что делали», сохраняем оригинал
return Config{}, fmt.Errorf("loadConfig(%s): %w", path, err)
}
// ...
}
Правило: оборачивайте ошибку через%w, если хотите, чтобы вызывающий код мог «заглянуть внутрь» черезerrors.Isилиerrors.As. Используйте%v, если хотите спрятать внутреннюю ошибку от внешнего кода — например, чтобы не протекали детали реализации в публичном API.
Проверка ошибок: errors.Is и errors.As
Сравнение err == os.ErrNotExist перестаёт работать, как только ошибку обернули. Для проверки по цепочке обёрток стандартная библиотека предоставляет две функции.
errors.Is — проверка по значению
if errors.Is(err, os.ErrNotExist) {
fmt.Println("файл не найден")
}
errors.Is разворачивает цепочку обёрток и сравнивает каждый элемент с целевым значением. Работает, даже если ошибка обёрнута несколько раз.
errors.As — проверка по типу
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("проблема с путём:", pathErr.Path)
}
errors.As ищет в цепочке ошибку нужного типа и, если находит, записывает её в переданную переменную. Это способ добраться до полей конкретной структуры ошибки.
| Функция | Что проверяет | Когда использовать |
|---|---|---|
errors.Is(err, target) | Совпадение с конкретным значением-ошибкой | Сравнение с sentinel-ошибками вроде os.ErrNotExist, io.EOF |
errors.As(err, &target) | Совпадение с типом ошибки | Когда нужны поля структуры: путь, код, HTTP-статус |
err == nil | Отсутствие ошибки | Проверка «всё ли хорошо» после вызова функции |
Собственные типы ошибок
Для библиотек и сервисов полезно определить свой тип, чтобы вызывающий код мог различать категории проблем:
// ValidationError — ошибка валидации входных данных
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("поле %s: %s", e.Field, e.Message)
}
// использование
func parseAge(input string) (int, error) {
age, err := strconv.Atoi(input)
if err != nil {
return 0, &ValidationError{
Field: "age",
Message: "ожидается целое число",
}
}
if age < 0 || age > 150 {
return 0, &ValidationError{
Field: "age",
Message: "значение вне допустимого диапазона 0–150",
}
}
return age, nil
}
Вызывающий код проверяет через errors.As и решает, что делать — показать пользователю сообщение или вернуть 400 в API.
Sentinel-ошибки: когда использовать
Sentinel — это заранее объявленная переменная-ошибка на уровне пакета:
var ErrUserNotFound = errors.New("пользователь не найден")
Такие ошибки удобны, когда вызывающему коду нужно принять решение по конкретному условию. Проверяются через errors.Is.
Не создавайте sentinel на каждый чих — они становятся частью публичного API пакета. Если ошибка нужна только для логирования, обычного fmt.Errorf достаточно.
panic и recover — это не обработка ошибок
В Go есть panic, которая похожа на исключение: прерывает выполнение и раскручивает стек. Но предназначена она для действительно невосстановимых ситуаций — программист допустил логическую ошибку, нарушен инвариант, индекс вышел за пределы массива.
recover позволяет перехватить панику внутри defer. Некоторые HTTP-фреймворки (стандартный net/http в том числе) используют recover в middleware, чтобы один паникующий обработчик не уронил весь сервер. Но строить бизнес-логику на panic/recover — антипаттерн: это скрывает путь ошибки и ломает линейность, ради которой Go отказался от исключений.
Пять частых ошибок новичков
- Игнорирование ошибки. Написать
result, _ := someFunc()— самый быстрый способ получить необъяснимый баг. Линтерerrcheck(входит вgolangci-lint) ловит такие случаи — включите его в CI с первого дня. - Потеря контекста.
return errбез оборачивания. Когда ошибка дойдёт до верхнего уровня, в логе будет только «permission denied» без указания, какой файл и на каком этапе. - Двойное логирование. Функция логирует ошибку и возвращает её наверх, где она логируется снова. Правило: либо обработай (залогируй, верни пользователю), либо верни — не оба.
- Сравнение через == вместо errors.Is. Работает, пока ошибку никто не обернул. Потом ломается молча.
- panic вместо error. «Файл конфига не найден» — это не паника, это штатная ошибка, которую нужно вернуть.
Что делать дальше
Настройте golangci-lint с включённым errcheck и wrapcheck — они поймают большинство промахов. Прочитайте раздел Error handling and Go в официальном блоге — там коротко и с примерами объяснена философия подхода. Когда дойдёте до сетевого кода или работы с базами, обратите внимание на пакет errors в стандартной библиотеке Go 1.21+ — там появились удобства вроде errors.Join для объединения нескольких ошибок в одну.
Главное: не боритесь с if err != nil. Это не синтаксический мусор — это точки, в которых вы явно решаете, что делать с проблемой. Через пару недель привыкнете, а через пару месяцев начнёте ценить.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Error handling and Go — The Go Blog, Package errors — Go standard library documentation
Частые вопросы
Почему в Go нет try-catch?
Что будет, если я проигнорирую возвращённую ошибку?
Когда использовать %w, а когда %v в fmt.Errorf?
Можно ли использовать panic для обработки бизнес-ошибок?
Как собрать несколько ошибок в одну?
У меня другая версия Go — что-то из статьи может не работать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.