Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 21 из 25 ~20 мин +50 XP

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

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

Содержание урока · 14 разделов

Ошибки в 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> в обоих движках.

Вывод

Запустите код, чтобы увидеть вывод.

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

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

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

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

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

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

Функция возвращает (int, error). Вызов привёз res = 0 и 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()) } — тот же текст, но как явную строку: метод вытянул сообщение из ошибки.

Вывод

Запустите код, чтобы увидеть вывод.

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.

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

Прочитайте код и выберите, что он выведет:

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)
}

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

Самый короткий путь понять идиому — спроектировать проверку с нуля. Возьмите деление: язык целочисленного деления на ноль не прощает — в рантайме настоящей программы паника, а не ошибка. Значит, функция деления обязана проверять делитель сама и возвращать ошибку парой. Три решения на этапе проектирования. Первое: какие случаи неправильные — для деления он один, 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)?

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 — неранимый блок ниже покажет оба мира.

Вывод

Запустите код, чтобы увидеть вывод.

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. На каждой итерации 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.

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

Прочитайте код и выберите, что он выведет:

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)
}

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

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

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

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

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

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

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) }

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

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

ЗаписьЧто делаетВ песочнице
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; резерв не парсит — в ранимых нет

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

Термин

Что такое идиома (value, err) и каков её контракт?

нажмите, чтобы перевернуть
Определение

Функция возвращает пару: результат первым, ошибка error последней. err == nil — успех; err != nil — value равен нулевому значению типа и его нельзя использовать. Проверка — if err != nil после каждого вызова; приём пары: res, err := f(...), повторно через =.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Идиома (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/) под рукой.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Base · Несколько возвращаемых значений и ошибки)

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

Почему при печати 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 и собственные типы ошибок — их разбирает продвинутый курс.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Go Pro25 уроков~14 чпрактика25 уроков уже доступны

Продвинутые возможности Go: от замыканий до рефлексии

Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.

  • Писать и читать конкурентный код: go-функции, каналы, select
  • Управлять жизненным циклом операций через context
  • Проектировать API на интерфейсах
  • Использовать дженерики без злоупотребления