---
title: "Ошибки как значения"
description: "Программа, которая работает только на правильных данных, бесполезна. Пользователь введёт 8o80 вместо 8080, файла не окажется на диске, сервер не ответит. Вопрос не в том, случится…"
canonical: https://graphlms.ru/go-basics/book/errors
course: "Основы Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Ошибки как значения

> Источник: https://graphlms.ru/go-basics/book/errors — курс «Основы Go», GraphLMS.

## О чём глава

Программа, которая работает только на правильных данных, бесполезна. Пользователь
введёт `8o80` вместо `8080`, файла не окажется на диске, сервер не ответит. Вопрос
не в том, случится ли ошибка, а в том, что программа сделает дальше.

Во многих языках ошибка — это исключение: где-то глубоко что-то пошло не так,
управление «выпрыгивает» из функции и летит вверх, пока кто-нибудь его не
поймает. По коду не видно, какая строка может выпрыгнуть. В Go **ошибка — это
обычное значение**, которое функция возвращает наравне с результатом. Вы уже
видели это в `strconv.Atoi` и `safeDiv`. Каждая проверка записана явно, и, читая
код, вы видите все места, где что-то может пойти не так.

Из этого вытекает вся идиоматика главы: проверка `if err != nil`, создание
ошибок через `errors.New` и `fmt.Errorf`, аккуратное оборачивание одной ошибки
в другую с помощью `%w`, и разбор «что именно случилось» через `errors.Is` и
`errors.As`. В конце разберём `panic`/`recover` — механизм, который похож на
исключения, но нужен крайне редко, и поймём, почему его не стоит тащить в
повседневный код.

## Ошибка — это просто значение

В языке есть встроенный интерфейс `error` (интерфейсы — тема [прошлой главы](/go-basics/book/interfaces)). Он крошечный:

```go
type error interface {
    Error() string
}
```

Любой тип, у которого есть метод `Error() string`, уже является ошибкой. Когда
функция может не справиться, она по соглашению возвращает `error` **последним**
значением. Всё хорошо — возвращаем `nil`. Что-то пошло не так — возвращаем не-nil
ошибку. Что происходит с остальными результатами, определяет контракт функции: они могут содержать полезные частичные данные.

Вызывающий код сразу же это значение проверяет. Отсюда самая узнаваемая
конструкция в Go:

```go play
package main

import (
	"fmt"
	"strconv"
)

func main() {
	n, err := strconv.Atoi("42")
	if err != nil {
		fmt.Println("не число:", err)
		return
	}
	fmt.Println("разобрали:", n*2)
}
```

`strconv.Atoi` возвращает два значения: число и ошибку. Мы первым делом смотрим
на `err`. Если она не `nil` — печатаем и выходим из `main` через `return`, не трогая `n`
(там лежит ноль, и доверять ему нельзя). Если `nil` — спокойно работаем с числом. Запустите пример:
строка `"42"` разбирается, на экране будет `разобрали: 84`.

Поменяйте в коде `"42"` на `"abc"` — и сработает ветка с ошибкой. Это и есть весь
смысл подхода: путь успеха и путь ошибки видны рядом, в одном месте, без
спрятанных прыжков.

## Идиома `if err != nil`

Новичков иногда смущает, что таких проверок в коде много. Это нормально и
осознанно: Go делает обработку ошибок явной, чтобы её **нельзя было случайно
пропустить**. Базовый шаблон такой:

```go
result, err := doSomething()
if err != nil {
    // обработали: вернули выше, залогировали, подставили запасной вариант…
    return err
}
// сюда дошли — значит err == nil и result валиден
```

Несколько практичных правил:

- Проверяйте ошибку **сразу** после вызова, пока контекст свежий.
- При `err != nil` проверьте контракт функции. Например, `io.Reader.Read` может вернуть прочитанные байты вместе с ошибкой — их обрабатывают до проверки ошибки.
- Чаще всего ошибку не «глотают», а **возвращают выше** — пусть решает тот, кто
  вызвал. Решение «что делать» обычно принимается ближе к верху программы.

## Создаём ошибки: `errors.New` и `fmt.Errorf`

Когда ваша собственная функция хочет сообщить о проблеме, ошибку нужно создать.
Простейший способ — `errors.New` с фиксированным текстом:

```go play
package main

import (
	"errors"
	"fmt"
)

func half(n int) (int, error) {
	if n%2 != 0 {
		return 0, errors.New("число нечётное, пополам не делится")
	}
	return n / 2, nil
}

func main() {
	for _, n := range []int{8, 7} {
		res, err := half(n)
		if err != nil {
			fmt.Printf("half(%d): %v\n", n, err)
			continue
		}
		fmt.Printf("half(%d) = %d\n", n, res)
	}
}
```

Вывод:

```
half(8) = 4
half(7): число нечётное, пополам не делится
```

Если в текст нужно подставить данные (что за значение, какой файл, какой код),
берут `fmt.Errorf` — это как `fmt.Sprintf`, только результат сразу `error`:

```go
return fmt.Errorf("не делится: получили %d", n)
```

Маленькое, но важное соглашение про текст ошибки: пишите его с маленькой буквы и
без точки в конце. Причина — ошибки часто склеивают друг с другом в цепочку, и
красивее, когда `«внешний контекст: внутренняя причина»` читается одной строкой,
а не как набор предложений с заглавными буквами посередине.

## Оборачивание: `%w` и `errors.Is`

Часто ошибка от нижнего слоя сама по себе малоинформативна. Хочется добавить
контекст («не смог загрузить профиль»), но при этом **не потерять** исходную
причину. Для этого в `fmt.Errorf` есть особый глагол `%w` (от *wrap* —
обернуть). Он вкладывает одну ошибку внутрь другой, сохраняя к ней доступ.

А чтобы потом спросить «а нет ли где-то в этой цепочке вот такой конкретной
ошибки?», используют `errors.Is`. Он разворачивает цепочку слой за слоем и
сравнивает с образцом:

```go play
package main

import (
	"errors"
	"fmt"
)

var errNotFound = errors.New("запись не найдена")

func loadUser(id int) error {
	if id != 1 {
		// оборачиваем причину через %w, добавляя свой контекст
		return fmt.Errorf("loadUser(%d): %w", id, errNotFound)
	}
	return nil
}

func main() {
	err := loadUser(42)
	fmt.Println(err)
	fmt.Println("это errNotFound?", errors.Is(err, errNotFound))
}
```

Вывод:

```
loadUser(42): запись не найдена
это errNotFound? true
```

Здесь происходит сразу две вещи. Во-первых, `err` печатается как
`loadUser(42): запись не найдена` — мы видим и свой контекст, и причину.
Во-вторых, `errors.Is(err, errNotFound)` возвращает `true`, **хотя** мы сравниваем
обёртку с тем, что лежит у неё внутри. Именно для этого нужен `%w`: вызывающий
код может реагировать на конкретную причину, не разбирая строку руками.

Заметьте важную деталь: образец `errNotFound` объявлен один раз как переменная
пакета (так называемая *sentinel*-ошибка). Сравнивать ошибки по тексту нельзя —
текст меняется. Сравнивают по значению-образцу, и `errors.Is` делает это
правильно даже сквозь несколько уровней оборачивания.

## `errors.As`: добраться до типа ошибки

`errors.Is` отвечает на вопрос «это *та самая* ошибка?». Иногда вопрос другой:
«а это ошибка *такого типа*, и если да — дай мне её поля». Многие ошибки
стандартной библиотеки — это структуры с полезными полями. Например,
`strconv.Atoi` на плохой строке возвращает `*strconv.NumError`, у которого есть
поля `Func` и `Num`. `errors.As` ищет в цепочке ошибку нужного типа и, найдя,
записывает её в вашу переменную:

```go play
package main

import (
	"errors"
	"fmt"
	"strconv"
)

// parsePort превращает строку в номер порта, оборачивая ошибку парсинга.
func parsePort(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, fmt.Errorf("разбор порта: %w", err)
	}
	return n, nil
}

func main() {
	_, err := parsePort("8o80") // опечатка: буква вместо нуля

	// strconv.Atoi возвращает *strconv.NumError — добираемся до его полей.
	var ne *strconv.NumError
	if errors.As(err, &ne) {
		fmt.Printf("не удалось разобрать %q в функции %s\n", ne.Num, ne.Func)
	} else {
		fmt.Println("какая-то другая ошибка:", err)
	}
}
```

Вывод: `не удалось разобрать "8o80" в функции Atoi`.

Обратите внимание на `errors.As(err, &ne)` — вторым аргументом передаётся
**адрес** указателя `ne`. Если в цепочке найдётся `*strconv.NumError`, функция
вернёт `true` и положит её в `ne`, после чего можно читать `ne.Num`, `ne.Func`.
Так вы достаёте структурированные данные из ошибки, а не парсите строку.

У `*strconv.NumError` есть и третье поле — `Err`, в котором лежит сама причина
(`strconv.ErrSyntax` для нечисловой строки или `strconv.ErrRange` для перебора).
Именно через него работает `errors.Is(err, strconv.ErrSyntax)` — то есть тот же
тип ошибки можно матчить и по типу через `As`, и по причине через `Is`.

Точно так же `errors.As` работает с **вашими собственными** типами ошибок.
Объявите структуру с методом `Error() string` — и её значение уже ошибка:

```go play
package main

import (
	"errors"
	"fmt"
)

type ValidationError struct {
	Field string
}

func (e *ValidationError) Error() string {
	return "невалидное поле: " + e.Field
}

func save(email string) error {
	if email == "" {
		return fmt.Errorf("сохранение пользователя: %w", &ValidationError{Field: "email"})
	}
	return nil
}

func main() {
	err := save("")
	fmt.Println(err)

	var ve *ValidationError
	if errors.As(err, &ve) {
		fmt.Println("проблема в поле:", ve.Field)
	}
}
```

Вывод:

```text
сохранение пользователя: невалидное поле: email
проблема в поле: email
```

Вызывающий код достал из обёртки исходную `*ValidationError` и прочитал поле
`Field`, ничего не разбирая в тексте. Так, например, HTTP-сервер решает, ответить
«400: исправьте поле email» или «500: что-то сломалось у нас».

Короткое правило выбора: сравниваете с конкретным значением-образцом — `Is`;
хотите вытащить тип и его поля — `As`.

## `panic` и `recover`: и когда их НЕ надо

`panic` — это аварийная остановка. Когда происходит паника, обычное выполнение
функции прекращается, отложенные `defer` всё же отрабатывают, и управление летит
вверх по стеку. Если панику никто не перехватил — программа падает с трейсом.

Звучит как исключение, и технически это близко. Но в Go так **не** обрабатывают
ожидаемые ошибки. Файла нет, число не распарсилось, пользователь не найден — это
не повод паниковать, это обычные `error`, которые надо вернуть и проверить.
`panic` оставляют для ситуаций, когда продолжать просто бессмысленно: нарушен
инвариант, который «не может» нарушиться, обязательная зависимость не
инициализирована, в map по ключу, который обязан существовать, ничего нет.
То есть для багов, а не для штатных исходов.

`recover` умеет перехватить панику — но **только** при непосредственном вызове из отложенной функции в той же горутине. Он гасит
панику и возвращает то значение, с которым паниковали. Классический и редкий
уместный сценарий: на границе компонента не дать одной плохой операции уронить
всю программу, превратив панику в обычную ошибку:

```go play
package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("восстановились после паники: %v", r)
		}
	}()
	return a / b, nil // при b == 0 здесь случится паника
}

func main() {
	if r, err := safeDivide(10, 2); err == nil {
		fmt.Println("10 / 2 =", r)
	}

	if _, err := safeDivide(10, 0); err != nil {
		fmt.Println("поймали:", err)
	}
}
```

Вывод:

```text
10 / 2 = 5
поймали: восстановились после паники: runtime error: integer divide by zero
```

Что здесь происходит. Целочисленное деление на ноль в Go — паника: среда
выполнения сама её поднимает с сообщением `runtime error: integer divide by zero`.
Обычно программа на этом упала бы. Но в функции есть `defer` с `recover`: при
выходе из функции он ловит панику, и вместо падения мы заполняем именованный
результат `err`. Вызывающий код получает обычную ошибку и продолжает работать.

В настоящем коде делитель, конечно, проверяют заранее через `if b == 0` и
возвращают `error`. Пример нужен только чтобы показать механизм.

Две детали этого приёма: `recover` должен вызываться непосредственно из отложенной функции в той же горутине; чтобы
подменить результат функции, нужны **именованные** возвращаемые значения
(`result int, err error`) — иначе из `defer` до них не дотянуться. И всё же
помните: это инструмент для границ и для действительно фатального, а не замена
`if err != nil`.

## Ловушка: nil, который не равен nil

Эту ошибку делают и опытные разработчики, и о ней любят спрашивать на
собеседованиях. Функция объявляет переменную-указатель на свою ошибку и
возвращает её как `error`:

```go play
package main

import "fmt"

type ValidationError struct {
	Field string
}

func (e *ValidationError) Error() string {
	return "невалидное поле: " + e.Field
}

func validate(email string) error {
	var ve *ValidationError // nil-указатель
	if email == "" {
		ve = &ValidationError{Field: "email"}
	}
	return ve // ловушка: возвращаем указатель, даже когда он nil
}

func main() {
	err := validate("anna@example.com")
	fmt.Println(err == nil)
}
```

Вывод: `false`. Почта корректная, ошибки нет, а проверка `err != nil` скажет,
что ошибка есть.

Причина — в устройстве интерфейса из [прошлой главы](/go-basics/book/interfaces):
значение интерфейса — это пара «тип + данные». Интерфейс равен `nil`, только
когда пусты обе части. Здесь данные — `nil`-указатель, но тип известен:
`*ValidationError`. Пара не пустая, значит, и `err` не `nil`.

Правило простое: если ошибки нет, возвращайте литерал `nil`, а не переменную
конкретного типа:

```go
if email == "" {
    return &ValidationError{Field: "email"}
}
return nil
```

Выведет `false`: `err` хранит пару «тип `*ValidationError`, значение `nil`», а такой интерфейс не равен `nil`. Нажмите кнопку в примере выше и убедитесь.

## Частые ошибки

- **Игнорировать ошибку через `_`.** `res, _ := mayFail()` молча выбрасывает
  проблему, и потом вы отлаживаете «непонятно почему ноль». Проверяйте `err`
  всегда; пропуск допустим только когда вы *осознанно* и *доказуемо* знаете, что
  ошибки быть не может.
- **Сравнивать ошибки по тексту.** `if err.Error() == "not found"` хрупко: текст
  меняется, ломается из-за обёрток. Используйте sentinel-переменную и
  `errors.Is`, а для типов — `errors.As`.
- **Путать `%w` и `%v`.** `%v` просто вставляет текст ошибки, но **не**
  оборачивает — `errors.Is`/`errors.As` потом ничего внутри не найдут. Если
  причину важно сохранить для проверки — берите `%w`.
- **Возвращать nil-указатель как `error`.** Интерфейс с типом внутри не равен
  `nil`, даже если указатель пустой. Когда ошибки нет, пишите `return nil`.
- **Паниковать вместо возврата ошибки.** `panic` ради «ну тут точно всё плохо» в
  библиотечном коде — это сюрприз для того, кто вас вызвал. Возвращайте `error` и
  дайте вызывающему решать.

## Попробуйте сами

Напишите `parsePositive(s string) (int, error)`: преобразуйте строку через `strconv.Atoi`, сохраните причину ошибки через `%w` и отвергните число `<= 0`. Проверьте строки `"7"`, `"0"`, `"abc"`. В вызывающем коде сначала проверяйте `err`, затем используйте число. Через `errors.Is` проверьте, сохранилась ли причина `strconv.ErrSyntax` для `"abc"`.

### Проверка

Только `"7"` даёт `7, nil`. `"0"` и `"abc"` дают ошибку; для последнего входа `errors.Is(err, strconv.ErrSyntax)` возвращает `true`.

```go
func parsePositive(s string) (int, error) {
    n, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("положительное число %q: %w", s, err)
    }
    if n <= 0 {
        return 0, fmt.Errorf("число должно быть больше нуля: %d", n)
    }
    return n, nil
}
```

Нужны импорты `fmt`, `strconv` и, для проверки причины, `errors`. Нулевой результат при ошибке здесь часть контракта вашей функции; вызывающий не принимает его за успешное число.

## Что дальше

Следующая глава — [введение в дженерики](/go-basics/book/generics): как написать один алгоритм сразу для нескольких типов, не теряя проверок компилятора.
