Содержание урока · 14 разделов
- Ошибки в Go: идиома (value, err)
- Несколько return go: контракт пары
- errors.New: простейший способ создать ошибку
- fmt.Errorf: ошибка с деталями
- if err != nil: главная проверка языка
- Своя проверка: деление на ноль под контролем
- Sentinel-ошибки: ErrSomething по образцу ErrNoRows
- %w: обёртка с сохранением причины
- Ловушки: неполный приём и глухой игнор
- Почему в Go нет исключений
- Шпаргалка: ошибки и возвраты
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Ошибки в 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
GOpackage 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 до неранимого блока.
Цикл вызовов: проверка на каждом шаге
GOpackage 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».
Соберите программу: проверка числа
Соберите программу из строк: 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; резерв не парсит — в ранимых нет |
Каждый ранимый пример урока прогнан пробником на обоих движках песочницы. Совпадают: сигнатура с 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 в сообщениях ошибок — по привычке не используем в ранимых: резервный печатает его упрощённо.
Карточки: ошибки и возвраты
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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/) под рукой.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему при печати 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: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления