---
title: "Несколько возвращаемых значений и ошибки"
description: "Ошибки в go: идиома (value, err) и несколько return-значений, проверка if err != nil, errors.New и fmt.Errorf, sentinel-ошибки и обёртка %w без исключений."
course: "Go с нуля: базовый синтаксис и конструкции"
url: "https://golangmaster.ru/courses/go-basics/lessons/21-oshibki-i-vozvraty/"
published: "2026-09-19"
updated: "2026-09-28"
minutes: 20
---

# Несколько возвращаемых значений и ошибки

*Курс «Go с нуля: базовый синтаксис и конструкции», урок 21 из 25. ~20 минут. Обновлено 28.09.2026. Источник: https://golangmaster.ru/courses/go-basics/lessons/21-oshibki-i-vozvraty/*

Трижды анонсированная идиома (value, err) наконец получает своё имя. Ошибки в go — это не исключения и не коды возврата, а обычное значение из пары нескольких return-значений: результат и ошибка едут вместе. Здесь раскроем контракт пары, научимся создавать ошибки через errors.New и fmt.Errorf, проверять их через if err != nil, заводить sentinel-ошибки по образцу ErrNoRows и оборачивать причины через %w. Все примеры запускаются прямо в уроке.

### Ошибки в Go: идиома (value, err)

Всё обучение вы уже сталкивались с ошибками в go, не называя их вслух: comma-ok из карт возвращал значение и флаг, divmod из функций — частное и остаток. Теперь наступает черёд самого важного применения нескольких return-значений — сообщения о проблемах. Ошибки в go — не исключения и не коды возврата: функция возвращает два значения — результат и ошибку. Ошибка — это обычный тип с именем error; значение nil означает «всё хорошо», любое другое — «что-то пошло не так, вот описание». Никакой магии: ошибка — такое же значение, как число или строка, её можно хранить в переменной, печатать, передавать дальше.

Идиома (value, err) закреплена во всём языке: почти каждая функция стандартной библиотеки, которая может не справиться, возвращает пару — результат первым, ошибку последней. Нашли запись в файле — значение и nil; не нашли — ноль и описание причины. Такое соглашение решает сразу две проблемы: вызывающий код не может «забыть» проверить ошибку, потому что она буквально выпадает из функции вместе с результатом, а сам поток программы остаётся видимым — никаких прыжков в неизвестную точку, как бывает с исключениями в других языках.

В этом уроке мы пройдём весь путь: создание ошибок через errors.New и fmt.Errorf, проверку if err != nil — самую частую строчку боевого Go-кода, собственную проверку деления на ноль, sentinel-ошибки по образцу ErrNoRows, обёртку причин через %w и ловушки приёма пары. Каждый ранимый пример прогнан на обоих движках песочницы — вывод совпадает, а места, где движки расходятся, честно оговорены и вынесены в неранимые блоки.

### Песочница: первая ошибка

Запустите: divide(10, 2) вернула 5 и nil — проверка err != nil не сработала, напечаталось просто 5. Второй вызов divide(7, 0) вернул 0 и ошибку: сработала ветка if err != nil, напечаталось «ошибка: деление на ноль», и return завершил main раньше третьей печати. Обратите внимание на две формы: первый приём — через := (res и err объявляются), повторный — через = (переменные уже есть, как с divmod в уроке 20). Опыты. Первый: поменяйте вызовы местами — первым пойдёт divide(7, 0), и main закончится на первой же проверке: до удачного вызова дело не дойдёт, return внутри ветки работает. Второй: замените текст ошибки на «делитель равен нулю» и убедитесь, что печатается именно ваша строка. Третий: напечатайте пару целиком — fmt.Println(divide(10, 2)) — увидите «5 <nil>»: nil-ошибка печатается литералом <nil> в обоих движках.

```go
package main

import (
        "errors"
        "fmt"
)

// divide возвращает частное или ошибку при нулевом делителе
func divide(a, b int) (int, error) {
        if b == 0 {
                return 0, errors.New("деление на ноль")
        }
        return a / b, nil
}

func main() {
        res, err := divide(10, 2)
        if err != nil {
                fmt.Println("ошибка:", err)
                return
        }
        fmt.Println(res)

        res, err = divide(7, 0)
        if err != nil {
                fmt.Println("ошибка:", err)
                return
        }
        fmt.Println(res)
}
```

### Несколько return go: контракт пары

Запрос несколько return go обычно означает «как из функции вернуть сразу два значения» — ответ вы знаете: типы пишутся в скобках, return перечисляет значения через запятую. Но у пары (value, err) поверх механизма лежит контракт, и он важнее синтаксиса. Первое: порядок фиксирован — значение первым, ошибка последней. Не «ошибка и результат», не «что-то ещё и ошибка»: ошибка всегда завершает список, это читается в каждой сигнатуре языка одинаково.

Второе: при ошибке value — нулевое значение своего типа. divide при нулевом делителе вернула 0 — не мусор и не случайное число, а честный ноль, оговорённый return 0, err. Поэтому правило потребителя жёсткое: пока err != nil, результат не трогаем — там ноль, который ничего не значит. Печатать его можно, использовать в вычислениях — нельзя. Именно ради этого правила пары из ошибкой не смешивают с парами вроде (частное, остаток): остаток при частном имеет смысл, ноль при ошибке — нет.

Третье: nil — единственный знак успеха. Не нужно возвращать флаг «всё хорошо» отдельно или проверять результат на специальное «магическое» число. err == nil — всё хорошо, err != nil — проблема с текстом. Первый двухзначный возврат курса вы уже трогали, не называя: comma-ok из [урока о картах](/courses/go-basics/lessons/19-karty/) — v, ok := m[k] — это чтение карты, возвращающее значение и флаг наличия, а [divmod из урока о функциях](/courses/go-basics/lessons/20-funkcii/) — частное и остаток одной парой. Механизм один, семантика разная: comma-ok отвечает на вопрос «есть ли ключ», пара (value, err) — на вопрос «справилась ли функция».

Четвёртое: err из пары нельзя просто выбросить молча. Подчёркивание формально законно — res, _ := divide(5, 0) скомпилируется, — но это осознанное «я гарантирую, что ошибки не будет или она не важна». Ловушки такого игнора и неполного приёма одной переменной разберём отдельным разделом ниже, а пока запомните порядок чтения: сначала проверка err, потом работа с value.

### Проверьте себя: читать пару

**Вопрос:** Функция возвращает (int, error). Вызов привёз res = 0 и err != nil. Что это значит и что делать?

- ✅ Функция сообщила об ошибке; res — нулевое значение без смысла, его не использовать, обработать err
- Вычисление прошло успешно: частное честно получилось нулевым
- Функция упала с паникой: программа сейчас завершится сама
- err != nil — это просто отладочная подсказка, её можно игнорировать

Контракт пары: при err != nil значение value — нулевое значение типа, напечатанное из вежливости, а не результат. Работать с ним нельзя — сначала обработка ошибки. Паники тут ни при чём: ошибка — обычное возвращённое значение, поток программы не прерывается, решение принимает вызывающий код. А нулевой результат при успехе — тоже возможен, но именно тогда err == nil; различать их и призвана проверка.

### errors.New: простейший способ создать ошибку

Откуда берётся значение типа error? Из фабрик. Простейшая — errors.New: принимает строку и возвращает готовую ошибку с этим текстом. errors.New("деление на ноль") — вот и вся наука. Пакет errors подключается отдельно: import "errors" — рядом с fmt в блоке импорта.

Ошибку можно завести и без всяких функций: err := errors.New("что-то сломалось") — и это уже полноценное значение. fmt.Println(err) печатает текст; если err ещё не присвоили — var err error — печатается <nil>: нулевое значение типа error, знак «ошибки нет». Оба поведения пробником проверены — вывод одинаков в обоих движках.

У ошибки есть метод Error() string — он и превращает значение в текст. err.Error() возвращает ту же строку, что и печать: обычно fmt.Println(err) достаточен, Println вызывает Error() за вас. Метод пригодится, когда текст нужен именно как строка: склеить с префиксом, посчитать длину, передать в лог. Забавная деталь, которую вы уже знаете из урока о строках: len(err.Error()) считает байты, а не символы — для «oops» это 4, а для «нет данных» — 19, потому что кириллица занимает по два байта на букву.

Когда errors.New достаточен? Когда текст ошибки статичен: «деление на ноль», «список пуст», «не найдено». Как только в сообщение хочется подставить детали — какое число, какой индекс, какая строка — наступает очередь fmt.Errorf, о нём следующий раздел. Склеить строку оператором + можно и внутри errors.New — errors.New("не найдено: " + name) — законно и иногда удобно, но форматирование через Errorf читается чище.

### fmt.Errorf: ошибка с деталями

fmt.Errorf — сестра fmt.Printf среди фабрик ошибок: принимает строку формата с глаголами и аргументы, возвращает ошибку с готовым, отформатированным текстом. fmt.Errorf("слишком большое число: %d", n) — в сообщении будет конкретное число. Глаголы те же, что в печати: %d для целых, %s для строк, %v для «как есть». Детали в тексте ошибки — не украшение: строка «ошибка» говорит только о факте беды, строка «слишком большое число: 200» сразу называет виновника, и отладка сокращается вдвое.

Возвращается пара как обычно: return 0, fmt.Errorf(...) — ноль и ошибка. А при удаче — return n * n, nil. Обратите внимание, как это выглядит в приёме: fmt.Println(v, err) печатает оба значения через пробел — «144 <nil>» при успехе и «0 слишком большое число: 200» при ошибке. Nil-ошибка в печати всегда выглядит как <nil> — и это норма, не сбой: Println честно показывает, что вторая часть пары пуста. В ранимом примере ниже ровно эта пара строк — пробник сверил её в обоих движках.

Слово Errorf расшифровывается как error + f (format) — по той же схеме, что Printf и Sprintf: f в конце имени функции fmt всегда означает строку формата. Ошибиться в регистре легко: fmt.errorf не существует, фабрика пишется с большой буквы — Errorf, как и все экспортируемые имена языка. Ошибка — обычное значение, и потому её тип в сигнатуре пишется без скобок, если пара одна: square(n int) (int, error) — значение int, ошибка error. Скобки вокруг списка возвратов нужны, когда значений больше одного; писать (int, error) — идиома, без них не обойтись.

### Песочница: ошибка с деталями

Запустите: первая строка «144 <nil>» — успех, err пуст и печатается литералом <nil>. Вторая «0 слишком большое число: 200» — ошибка: value ноль, err несёт текст с подставленным числом — fmt.Errorf подставил 200 вместо %d. Опыты. Первый: вызовите square(101), square(100) и square(-5) — граница строгая: сотня проходит (10 000 и <nil>), сто один — нет. Второй: добавьте проверку if n < 0 { return 0, fmt.Errorf("отрицательное: %d", n) } выше основной — проверяйте в нужном порядке: у -5 сработает именно новая ветка. Третий: напечатайте err.Error() в ветке if err != nil { fmt.Println(err.Error()) } — тот же текст, но как явную строку: метод вытянул сообщение из ошибки.

```go
package main

import "fmt"

// square отказывается возводить в квадрат числа больше сотни
func square(n int) (int, error) {
        if n > 100 {
                return 0, fmt.Errorf("слишком большое число: %d", n)
        }
        return n * n, nil
}

func main() {
        v, err := square(12)
        fmt.Println(v, err)
        v, err = square(200)
        fmt.Println(v, err)
}
```

### if err != nil: главная проверка языка

Создать ошибку — полдела. Вторая половина — проверить её у вызывающего. Строчка if err != nil — самая узнаваемая конструкция Go: она встречается в боевом коде чаще любого другого оператора, и её пишут после каждого вызова, который умеет подводить. Схема неизменна: приём пары, проверка err, реакция. Реакция бывает трёх видов — напечатать и уйти (fmt.Println("ошибка:", err) и return), вернуть проблему выше (return 0, err — пробросить парой дальше), или справиться на месте (подставить значение по умолчанию, повторить попытку). В runner-е урока вы видели первый вид: печать и return — main честно сообщает причину и заканчивается.

Проверку удобно писать в форме оператора с объявлением: if err := divide(7, 0); err != nil { ... } — err объявляется прямо в заголовке if и живёт только внутри проверки. Экономится строка, а область видимости не мусорится — err не протекает в код ниже. Когда ошибка нужна и после проверки (например, в else-ветке или дальше по функции), приём делают заранее: res, err := divide(...) — и then обычный if err != nil без объявления.

Повторный вызов той же функции не требует второй :=: res, err = divide(...) — переменные уже есть, присваивание через =, ровно как с comma-ok и divmod. Одна переменная err переживает цепочку вызовов: после каждого присваивания она снова либо nil, либо свежая ошибка. Проверка на удачу — if err == nil — тоже законна, но идиоматичнее спрашивать о беде: err != nil. А ветка else берётся редко: если ошибки нет — просто работаем дальше без вложенного уровня. Правило читаемости: обработали err — сразу return; код счастливого пути держится левым краем, без лестницы вложенных else.

### Что выведет код: печать пары

```go
package main

import "fmt"

func divide(a, b int) (int, error) {
        if b == 0 {
                return 0, fmt.Errorf("деление на %d", b)
        }
        return a / b, nil
}

func main() {
        res, err := divide(20, 4)
        fmt.Println(res, err)
        res, err = divide(1, 0)
        fmt.Println(res, err)
}
```

**Что выведет программа?**

- ✅ 5 <nil> и 0 деление на 0: при успехе err — nil, при ошибке value — ноль и текст
- 5 5 и 0 0: err ведёт себя как второй результат вычисления
- 5 <nil> и 5 <nil>: err не меняется при повторном приёме через =
- Программа упадёт: печатать nil-ошибку нельзя

divide(20, 4) возвращает 5 и nil — печатается «5 <nil>». Повторный приём через = кладёт в те же переменные 0 и ошибку «деление на 0» — печатается «0 деление на 0». Печать пары Println(res, err) — быстрый способ заглянуть в оба значения сразу: nil показывается литералом <nil>. Вывод одинаков в обоих движках — блок ранимый.

### Своя проверка: деление на ноль под контролем

Самый короткий путь понять идиому — спроектировать проверку с нуля. Возьмите деление: язык целочисленного деления на ноль не прощает — в рантайме настоящей программы паника, а не ошибка. Значит, функция деления обязана проверять делитель сама и возвращать ошибку парой. Три решения на этапе проектирования. Первое: какие случаи неправильные — для деления он один, if b == 0. Второе: какой текст вернуть — конкретный и без лишних слов: «деление на ноль». Третье: что вернуть в value — по контракту ноль: return 0, errors.New("деление на ноль").

Такая проверка называется guard — часовой в начале функции: частный случай отработан — выход с ошибкой, общий случай — ниже, в return a / b, nil. Вы уже писали guard в уроке о функциях — там он возвращал безопасный ноль молча; разница ровно одна: теперь выход несёт объяснение, и вызывающий код узнаёт причину, а не угадывает её по нулю.

Дальше — расплата за честность: каждый вызывающий обязан решить, что делать с ошибкой. В main мы печатали и выходили. В цикле — продолжаем со следующей пары: v, err := safeDiv(...), if err != nil { печать; continue } — итерация пропущена, цикл живёт дальше. В другом коде — подставляем запасное значение или пробрасываем ошибку выше со своим контекстом. Одинаковый скелет, разная реакция — и в этом смысл: решение о судьбе ошибки принимает тот, у кого достаточно знаний о программе. Ранимый пример с циклом из трёх пар ждёт ниже — на нём видно, как ошибка не прерывает обработку списка, а лишь помечает одну строку.

### Проверьте себя: неполный приём

**Вопрос:** divmod(a, b int) (int, int) возвращает частное и остаток. Что произойдёт при q := divmod(17, 5)?

- ✅ Настоящий компилятор Go отвергнет запись: два значения нельзя присвоить одной переменной; оба движка песочницы молча возьмут первое
- Компилятор отбросит остаток и выдаст предупреждение
- q получит остаток — он второй в списке возвратов
- Программа упадёт во время выполнения с ошибкой присваивания

Это ловушка из урока 20, ставшая важной именно здесь: неполный приём двух значений — ошибка компиляции в настоящем Go (assignment mismatch), а оба движка песочницы молча берут первое значение и возвращают 3: основной yaegi здесь расходится с настоящим компилятором, резервный просто упрощает. Полагаться на такое нельзя нигде: пишите две переменные q, r := divmod(17, 5) или игнорьте явно — _, r. Для пар (value, err) это вдвойне опасно: q := divide(7, 0) в песочнице вернёт ноль и спрячет ошибку — код выглядит работающим, а проблема замолчана.

### Sentinel-ошибки: ErrSomething по образцу ErrNoRows

Когда один и тот же текст ошибки нужен в нескольких местах или его хочется проверять программно, текст выносят в переменную уровня пакета: var ErrDivZero = errors.New("деление на ноль"). Такая переменная называется sentinel-ошибкой — сторожевой: она одна на весь пакет, объявлена заранее и возвращается функциями как есть — return 0, ErrDivZero. Имя по стилю языка: Err и продолжение с большой буквы — ErrDivZero, ErrEmpty, ErrNotFound.

Стандартная библиотека набита sentinel-ошибками — это стиль ErrNoRows: пакет database/sql объявляет sql.ErrNoRows — «строк по запросу не нашлось», пакет io — io.EOF — «данные кончились», пакет strconv — ErrSyntax — «не удалось разобрать строку». Заметили закономерность? Конкретное имя после Err, объявление в пакете, один смысл на всё API. Ваш код делает то же самое своими руками: ErrDivZero в runner-е ниже — мини-версия ErrNoRows, только про деление.

Зачем это нужно. Первое — переиспользование: текст написан один раз, все функции пакета возвращают одинаковую формулировку. Второе — документация: sentinel в сигнатуре пакета — обещание «эта ошибка возможна», читатель API видит список ожидаемых проблем. Третье — сравнение: теперь можно спросить, является ли вернувшаяся ошибка именно этой. Инструмент сравнения — errors.Is(err, ErrDivZero) — и вот тут начинаются песочничные тонкости: основной движок — настоящий Go — сравнение исполняет, резервный не знает члена Is в пакете errors, поэтому блок с ним ниже сделан неранимым с честной оговоркой. Печатать же sentinel и оборачивать его через %w умеют оба движка — этим и займёмся.

### Песочница: sentinel и обёртка %w

Запустите: первая строка 0 — value при ошибке по контракту ноль. Вторая «8 / 0: деление на ноль» — текст из fmt.Errorf: подставлены делимое и делитель, а глагол %w втащил за собой текст sentinel-ошибки. Третья «3 <nil>» — удача без сюрпризов. Опыты. Первый: оберните ещё раз — err2 := fmt.Errorf("в main: %w", err) и напечатайте err2: цепочка причин вырастет — «в main: 8 / 0: деление на ноль». Второй: измените текст sentinel-а — «нулевые делитель» — и убедитесь, что новая формулировка проехала внутрь обёртки: sentinel меняется в одном месте. Третий: попробуйте errors.Is(err, ErrDivZero) — резервный движок ответит «у пакета errors нет члена Is»: в песочнице сравнение sentinel-ов недоступно, в настоящем Go та же программа печатает true — неранимый блок ниже покажет оба мира.

```go
package main

import (
        "errors"
        "fmt"
)

// ErrDivZero — sentinel: одна ошибка на весь пакет
var ErrDivZero = errors.New("деление на ноль")

func divide(a, b int) (int, error) {
        if b == 0 {
                return 0, fmt.Errorf("%d / %d: %w", a, b, ErrDivZero)
        }
        return a / b, nil
}

func main() {
        res, err := divide(8, 0)
        fmt.Println(res)
        fmt.Println(err)
        res, err = divide(9, 3)
        fmt.Println(res, err)
}
```

### errors.Is: сравнение с sentinel

```go
package main

import (
        "errors"
        "fmt"
)

var ErrDivZero = errors.New("деление на ноль")

func divide(a, b int) (int, error) {
        if b == 0 {
                return 0, fmt.Errorf("проверка %d: %w", a, ErrDivZero)
        }
        return a / b, nil
}

func main() {
        _, err := divide(5, 0)
        if errors.Is(err, ErrDivZero) {
                fmt.Println("это точно деление на ноль")
        }
        fmt.Println(errors.Is(err, ErrDivZero))
}
```

Кнопки нет: резервный движок не знает члена Is в пакете errors и падает с «у пакета errors нет члена Is» ещё до исполнения. Основной движок — настоящий Go — исполняет честно: errors.Is сравнивает обёрнутую ошибку с sentinel-ом сквозь %w и печатает «это точно деление на ноль», затем true. Для ошибки из удачного вызова — errors.Is(nil, ErrDivZero) — вернулось бы false: nil ни с чем не совпадает. Это стандартный способ проверять известные ошибки стандартной библиотеки в стиле ErrNoRows: if errors.Is(err, sql.ErrNoRows) { ... } — «строк не нашлось» обрабатывается отдельно от прочих сбоев. В ранимых примерах урока errors.Is не встречается — песочница его не берёт, но читать чужой код с ним теперь можно свободно.

### %w: обёртка с сохранением причины

Глагол %w в fmt.Errorf — отдельная суперсила: он вставляет исходную ошибку внутрь новой, сохраняя её значение, а не только текст. fmt.Errorf("%d / %d: %w", a, b, ErrDivZero) собирает сообщение из деталей и причины: напечатается «8 / 0: деление на ноль» — что делили, на что, и почему нельзя. Разница с подстановкой %v или склейкой строк принципиальна: %w хранит связь «новая ошибка является причиной-ошибкой», и по этой связи потом можно пробраться к корню — именно так errors.Is находит sentinel сквозь любые слои обёртки.

Обёртки вкладываются друг в друга: err := fmt.Errorf("поиск: %w", ErrNotFound), затем err2 := fmt.Errorf("слой 2: %w", err) — печатается «слой 2: поиск: не найдено». Так выглядит честная трассировка без исключений: каждый уровень добавляет свой контекст — функция, аргумент, операция — и передаёт проблему выше, ничего не теряя. Низовой уровень знает «что сломалось», верхние — «в какой операции»: вместе они дают полный рассказ.

Эта тема — вход в большой мир обработки ошибок: сравнение через errors.Is, выделение конкретных типов через errors.As, собственные типы ошибок с полями-деталями. Продвинутый курс отводит ей целый урок о заворачивании ошибок — там будут и цепочки причин, и кастомные структуры-ошибки; здесь достаточно привычки: возвращая ошибку из глубины, оборачивайте её с контекстом — «не могу прочитать файл: %w» вместо голого переприсваивания. Печать %w-обёрток работает в обоих движках песочницы — ранимый runner выше это показал; вся магия сравнения остаётся за настоящим Go до неранимого блока.

### Цикл вызовов: проверка на каждом шаге

```go
package main

import (
        "errors"
        "fmt"
)

// safeDiv — защита от нулевого делителя
func safeDiv(a, b int) (int, error) {
        if b == 0 {
                return 0, errors.New("деление на ноль")
        }
        return a / b, nil
}

func main() {
        pairs := [][2]int{{10, 2}, {7, 0}, {9, 3}}
        for _, p := range pairs {
                v, err := safeDiv(p[0], p[1])
                if err != nil {
                        fmt.Println("пара", p[0], p[1], "— ошибка:", err)
                        continue
                }
                fmt.Println(v)
        }
}
```

Блок ранимый — вывод одинаков в обоих движках: 5, затем «пара 7 0 — ошибка: деление на ноль», затем 3. Пары собраны в срез массивов из двух чисел, range перебирает его по порядку — приём из [урока про range](/courses/go-basics/lessons/15-range/). На каждой итерации v и err объявляются заново — := внутри цикла создаёт свежую пару, ошибка предыдущего шага не переживает итерацию. Ошибка второй пары не прерывает обработку: печать причины и continue перекидывают цикл к третьей паре — так список данных переживает точечные сбои. Поменяйте порядок пар — сообщения последуют за данными: обработка привязана к итерации, а не к позиции в списке.

### Ловушки: неполный приём и глухой игнор

У пары (value, err) две классические ловушки, и обе — на стороне приёма. Первая — неполный приём: q := divmod(17, 5) — одна переменная на два возврата. Настоящий компилятор Go это отвергает: assignment mismatch, два значения не умещаются в одно. А вот оба движка песочницы — и основной, и резервный — молча берут первое значение, и программа выглядит рабочей: основной yaegi здесь расходится с настоящим компилятором, резервный просто упрощает. Для пар с ошибкой эта мягкость особенно коварна: q := divide(7, 0) положит в q ноль, а ошибку выбросит — код напечатает ноль и промолчит о причине. Правило одно для всех сред: два возврата — две переменные или явное подчёркивание. Локальный компилятор поймает вашу опечатку, песочница — нет, так что привычка нужна независимо от среды.

Вторая ловушка — глухой игнор: res, _ := divide(5, 0). Подчёркивание формально законно — оба движка вернут ноль и три в примере ниже, — но ошибка выброшена без просмотра. Так писать стоит только с осознанной гарантией: «ошибка здесь невозможна или для программы безразлична». Таких мест в реальном коде — единицы, а вот байт-в-байт совпадающих с ними строк, где автор просто поленился, — тысячи. Рецепт: пока не уверены — не игнорьте; печать, проброс или обработка всегда лучше молчания. Quiz выше и predict ниже прогонят обе ловушки через вас лично.

И две мелочи напоследок. Сигнатуры с разными типами параметров — find(s []string, want string) — валидны в настоящем Go, но резервный не парсит их (квирк из урока о функциях), поэтому все ранимые примеры урока держатся групповой формы (a, b int) или одного параметра. А именованные возвраты с ошибкой — (q int, err error) — резервный движок не читает по той же причине, что и любые именованные результаты: форма настоящая, законная (пробник проверил на настоящем Go: 4 <nil> и 0 деление на ноль), но в песочнице только текстом, как договорились в уроке 20.

### Что выведет код: глухой игнор

```go
package main

import "fmt"

func divide(a, b int) (int, error) {
        if b == 0 {
                return 0, fmt.Errorf("деление на %d", b)
        }
        return a / b, nil
}

func main() {
        res, _ := divide(5, 0)
        fmt.Println(res)
        res, _ = divide(9, 3)
        fmt.Println(res)
}
```

**Что выведет программа?**

- ✅ 0 и 3: при ошибке value — ноль; подчёркивание не проверяет err — так делать не стоит
- деление на 0 и 3: печать сама вытянет ошибку из подчёркивания
- 5 и 3: при делении на ноль divide вернёт исходное делимое
- Паника: игнорировать ошибку через _ запрещено компилятором

Подчёркивание выбрасывает второе значение — err — не глядя. divide(5, 0) вернула 0 и ошибку: напечатался только ноль, причина потеряна. Второй вызов прошёл успешно — напечаталось 3. Оба движка дают одинаковый вывод — ловушка воспроизводится и в песочнице. Компилятор такое не ловит: это ошибка стиля, а не синтаксиса, поэтому её замечают только ревью и тесты. В боевом коде после res, _ читатель не может отличить «автор уверен, что ошибки не будет» от «автор забыл проверить».

### Почему в Go нет исключений

Во многих языках ошибка сигнализируется исключением: функция бросает, стек разматывается, ближайший обработчик ловит. В Go этот путь выбран сознательно против. Аргументы три. Первый — видимость потока: если ошибка — значение из пары, то каждая точка, где может случиться беда, видна в коде буквами if err != nil; с исключениями полный список возможных сбоев в функции выписать невозможно — бросить может любой вызов на любую глубину. Второй — цена управления: разматывание стека и ловля исключений дороже простого возврата значения, а Go строился для сервисов, где миллионы вызовов в секунду. Третий — принуждение: пара (value, err) физически не даёт проигнорировать ошибку случайно — значение выпадает наружу, и его нужно куда-то положить.

Плата известна — многословность: строка if err != nil повторяется десятки раз. Го-сообщество считает её честной ценой за читаемость: взамен код без скрытых путей исполнения, который можно читать сверху вниз и точно знать, где что может сорваться. Снизить шум помогает дисциплина из этого урока: обёртка с контекстом через %w, sentinel-ы для известных случаев, обработка в одном месте — в main или на границе сервиса.

Уточнение для честности картины: паника в Go существует — механизм аварийного выхода для неисправимых ситуаций вроде индекса за границей среза или деления на ноль в чистом коде; её разбор — panic и recover — живёт в продвинутом курсе, там же, где и defer. Но паника — не исключение и не канал обычной обработки: она для багов, а не для «файла не нашлось». Ошибки в go — это всегда пара значений и явная проверка: до указателей из урока 22 ещё два шага, а главный контракт взаимодействия функций между собой вы уже знаете.

### Вставьте пропуски: своя ошибка

Соберите проверку деления на ноль внутри литерала-функции: собранная программа печатает «деление на 0».

```go
divide := func(a, b int) (int, error) {
        if b == 0 {
                return 0, fmt.___("деление на %d", b)
        }
        return a / b, ___
}
res, err := divide(6, 0)
if ___ != nil {
        fmt.Println(err)
}
```

Подсказка: Первый пропуск — фабрика ошибок с форматированием из пакета fmt, имя с большой буквы. Второй — значение ошибки при успехе. Третий — имя переменной, куда легла ошибка из пары.

Правильные ответы в пропусках: `Errorf`, `nil`, `err`

### Соберите программу: проверка числа

Соберите программу из строк: check принимает число и возвращает пару — значение или ошибку с деталями. Собранный вывод — «7 <nil>» и «0 нужен положительный, а не 0».

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `func check(n int) (int, error) {`
4. `        if n <= 0 {`
5. `                return 0, fmt.Errorf("нужен положительный, а не %d", n)`
6. `        }`
7. `        return n, nil`
8. `}`
9. `func main() {`
10. `        v, err := check(7)`
11. `        fmt.Println(v, err)`
12. `        v, err = check(0)`
13. `        fmt.Println(v, err)`
14. `}`

### Шпаргалка: ошибки и возвраты

| Запись | Что делает | В песочнице |
| --- | --- | --- |
| func divide(a, b int) (int, error) | пара: значение и ошибка, err — последним | работает, вывод одинаков в обоих движках |
| return 0, errors.New("деление на ноль") | ошибка со статичным текстом, value — ноль | работает |
| return a / b, nil | успех: err — nil | работает; nil печатается как <nil> |
| res, err := divide(10, 2) | приём пары; повторно — через = | работает |
| if err != nil { ... } | главная проверка после каждого вызова | работает, включая форму if err := f(); err != nil |
| if err == nil { ... } | проверка удачи | работает |
| err.Error() | текст ошибки как строка | работает; len считает байты, кириллица — по два байта |
| fmt.Errorf("деление на %d", b) | ошибка с деталями: глаголы %d, %s, %v | работает |
| var ErrXxx = errors.New(...) | sentinel-ошибка уровня пакета, стиль ErrNoRows | работает |
| fmt.Errorf("...: %w", ErrXxx) | обёртка: детали + причина в одном тексте | работает; двойная обёртка тоже |
| errors.Is(err, ErrXxx) | сравнение с sentinel сквозь обёртки | резерв не знает члена Is: неранимо (настоящий Go: true) |
| res, _ := divide(5, 0) | глухой игнор ошибки — анти-паттерн | работает, но err потерян: напечатается 0 |
| q := divmod(17, 5) | неполный приём двух возвратов | настоящий Go не компилирует; резерв молча берёт первое |
| func find(s []string, want string) | сигнатура с разными типами параметров | валидна в настоящем Go; резерв не парсит — в ранимых нет |

> **Честно о песочнице: два движка.** Каждый ранимый пример урока прогнан пробником на обоих движках песочницы. Совпадают: сигнатура с error в возвратах — (int, error) и (string, error) — приём пары через := и повторный через =, ранний return с ошибкой, if err != nil, if err == nil, форма if err := f(); err != nil, errors.New, err.Error(), печать nil-ошибки как <nil>, fmt.Errorf с %d и %s, печать пары Println(v, err), обёртка %w и двойная обёртка, sentinel-переменные уровня пакета, глухой игнор res, _ и цикл с continue по ошибке. Расходятся или недоступны, все случаи проверены пробником: (1) errors.Is — резервный не знает члена Is («у пакета errors нет члена Is»), настоящий Go сравнивает верно — неранимо; (2) смешанные типы параметров find(s []string, want string) — настоящий Go валиден, резервный не парсит («ожидался тип, получено ","»), в ранимых примерах групповая форма или один параметр; (3) неполный приём q := divmod(17, 5) — настоящий компилятор Go отвергает (assignment mismatch), а ОБА движка песочницы молча берут первое значение (основной yaegi здесь расходится с компилятором); (4) именованные возвраты с error — (q int, err error) — резервный не парсит сигнатуру, форма настоящая и проверена пробником на настоящем Go; (5) %q в сообщениях ошибок — по привычке не используем в ранимых: резервный печатает его упрощённо.

### Карточки: ошибки и возвраты

- **Что такое идиома (value, err) и каков её контракт?** — Функция возвращает пару: результат первым, ошибка error последней. err == nil — успех; err != nil — value равен нулевому значению типа и его нельзя использовать. Проверка — if err != nil после каждого вызова; приём пары: res, err := f(...), повторно через =.
- **Чем errors.New отличается от fmt.Errorf?** — errors.New("текст") — статичное сообщение без подстановок. fmt.Errorf("шаблон %d", x) — то же + форматирование: детали (число, строка) в тексте через глаголы %d, %s, %v. Ошибка — значение: печатается Println(err), текст достаётся err.Error(), nil-ошибка печатается как <nil>.
- **Как писать проверку if err != nil идиоматично?** — Приём и проверка рядом: res, err := f(); if err != nil { return 0, err } — обработал и ушёл, счастливый путь без лестницы else. Короткая форма с объявлением в заголовке: if err := f(); err != nil { ... } — err живёт только внутри if. Одна переменная err переживает цепочку вызовов через =.
- **Что такое sentinel-ошибка и стиль ErrNoRows?** — Переменная уровня пакета: var ErrDivZero = errors.New("деление на ноль") — одна на весь пакет, возвращается как есть. Стандартная библиотека называет их Err + смысл: sql.ErrNoRows — строк нет, io.EOF — данные кончились. Сравнение с ней — errors.Is(err, ErrDivZero) (в песочнице члена Is нет — неранимо).
- **Что делает %w в fmt.Errorf?** — Оборачивает исходную ошибку в новую с сохранением значения: fmt.Errorf("не могу прочитать: %w", err) — текст содержит и контекст, и причину. Обёртки вкладываются: каждый уровень добавляет свой рассказ. По связи «является ошибкой» errors.Is находит sentinel сквозь слои. Развитие темы — урок о заворачивании ошибок в продвинутом курсе.
- **Почему в Go нет исключений и какие у пары ловушки?** — Ошибка — значение: каждая точка сбоя видна буквами if err != nil, поток не прыгает, вызовы дёшевы; паника есть, но только для неисправимых багов, не для обычной обработки. Ловушки: неполный приём q := divmod(17, 5) — Go не компилирует, резерв берёт первое; глухой игнор res, _ := f(...) — ошибка потеряна; на err != nil value не трогаем — там ноль.

## Коротко: что запомнить

- Идиома (value, err): значение первым, ошибка последней; err == nil — успех, при err != nil value — нулевое значение и его не использовать.
- errors.New("текст") — статичная ошибка; fmt.Errorf("шаблон %d", x) — с деталями; err.Error() достаёт текст, nil печатается как <nil>.
- if err != nil — самая частая строчка Go: печать и return, проброс выше или обработка на месте; форма if err := f(); err != nil ограничивает область видимости.
- Своя проверка деления на ноль — guard в начале функции: return 0, errors.New("деление на ноль") при b == 0, иначе return a / b, nil.
- Sentinel-ошибки: var ErrXxx = errors.New(...) уровня пакета, стиль ErrNoRows из стандартной библиотеки; сравнение — errors.Is (в резерве недоступно, неранимо).
- %w оборачивает ошибку с сохранением причины: fmt.Errorf("контекст: %w", err); цепочки обёрток дают полную трассировку без исключений.
- Ловушки: неполный приём q := divmod(17, 5) настоящий компилятор отвергает, оба движка песочницы молча берут первое; глухой игнор res, _ теряет ошибку; исключений в Go нет — ошибка это значение.

## Домашнее задание

Соберите мини-валидатор на парах. Шаг первый: divide(a, b int) (int, error) с guard-ом на нулевой делитель через errors.New — прогоните на парах 10/2, 7/0 и 9/3, печатая пару целиком. Шаг второй: parseDigits(s string) (int, error), разбирающая строку из цифр в число — при нецифровом символе верните fmt.Errorf("не цифра: %s", s) — проверьте на «42», «x8» и пустой строке. Шаг третий: sentinel var ErrEmpty = errors.New("список пуст") для функции first(s []string) (string, error), возвращающей первый элемент — вызовите на nil-срезе и на срезе из одного слова. Шаг четвёртый: цикл по срезу пар с if err != nil, печатью причины и continue — список из пяти пар, две из которых ошибочные. Шаг пятый: оберните ошибку из шага первого в fmt.Errorf("вычисление: %w", err) и напечатайте цепочку. [Оглавление курса](/courses/go-basics/) под рукой.

## Вопросы и ответы по уроку

### Почему при печати nil-ошибки выводится <nil> — это сбой?

Нет, это норма языка. fmt.Print выводит нулевое значение типа error литералом <nil> — так в обоих движках песочницы и в настоящем Go. Печать пары Println(v, err) — быстрый способ увидеть оба значения: «5 <nil>» означает успех с результатом 5, «0 деление на ноль» — ошибку с нулевым значением. В боевом коде err при успехе не печатают — его проверяют на nil и при удаче просто не трогают.

### Когда брать errors.New, а когда fmt.Errorf?

errors.New — когда текст ошибки статичен: «деление на ноль», «список пуст». fmt.Errorf — когда в сообщение нужны детали: fmt.Errorf("нужен положительный, а не %d", n) называет конкретное число. Склейка строк внутри errors.New законна, но Errorf читается чище и не путается в пробелах. Если оборачиваете чужую ошибку — только fmt.Errorf с глаголом %w: он сохраняет исходную ошибку внутри новой, и errors.Is сможет найти причину сквозь слои.

### Как проверить, что ошибка именно та самая, — например, ErrNoRows?

Функцией errors.Is(err, sql.ErrNoRows): она сравнивает ошибку с sentinel-ом, в том числе сквозь обёртки %w — так в стиле ErrNoRows отделяют «строк не нашлось» от прочих сбоев базы. В песочнице errors.Is не исполняется: резервный движок не знает члена Is в пакете errors, поэтому в уроке этот блок неранимый с честной оговоркой, а вывод настоящего Go показан в объяснении. Для сложных случаев есть errors.As и собственные типы ошибок — их разбирает продвинутый курс.
