Урок курса
Тип error: возврат, проверка и оборачивание
Golang с нуляВ прошлом уроке функция divide сигнализировала о делении на ноль вторым возвращаемым значением типа bool. Это работало, но информация скудная: вызывающий код знает лишь «что-то пошло не так», а не почему. Go решает эту проблему типом error — он несёт текстовое описание причины сбоя и встроен в язык как первоклассный интерфейс.
Интерфейс error: контракт, nil и соглашение о позиции
В Go error определён примерно так:
type error interface {
Error() string
}
Это встроенный интерфейс с одним методом. Любой тип, реализующий Error() string, автоматически удовлетворяет этому интерфейсу — никаких явных объявлений не нужно.
Ключевой момент: нулевое значение интерфейса — nil. Именно nil — единственный способ сказать «всё прошло успешно». Если функция вернула nil в позиции error, значит сбоя не было.
Посмотрите на типичную сигнатуру:
func validatePositive(n int) (int, error)
Здесь два возвращаемых значения. Если error не равен nil, нужно обработать ошибку и использовать остальные возвращаемые значения только так, как разрешает контракт конкретной функции. Например, validatePositive при ошибке возвращает 0 просто как неиспользуемое значение — читать в нём смысл не стоит. В других функциях контракт может быть иным: некоторые намеренно возвращают полезный частичный результат вместе с ошибкой (например, io.Reader.Read сообщает, сколько байт успело прочитаться до сбоя). Это соглашение о поведении конкретной функции, а не ограничение компилятора.
Почему error принято ставить последним? Читая сигнатуру слева направо, вы сначала видите «что возвращает функция при успехе», а потом — «что вернётся при сбое». Если поставить error первым, код вызова выглядел бы как err, result := f() — взгляд каждый раз спотыкается о err до того, как понял, что вообще вызывается.
Сравните (float64, bool) из предыдущего урока с (float64, error): оба паттерна используют несколько возвращаемых значений, но error добавляет к факту сбоя его описание. Вызывающий код может прочитать это описание, залогировать, передать выше — bool на такое неспособен.
Создание и оборачивание ошибок: errors.New, fmt.Errorf с %w и errors.Is
Самый простой способ создать ошибку — errors.New:
import "errors"
var ErrNotPositive = errors.New("число должно быть положительным")
errors.New возвращает значение типа error с фиксированным сообщением. Объявлять такие переменные на уровне пакета — стандартная практика: они называются сигнальными ошибками (sentinel errors) и позволяют сравнивать конкретные причины сбоя.
Но что если функция нашла ошибку глубже и хочет добавить контекст, не теряя оригинал? Здесь нужен fmt.Errorf с глаголом %w:
import "fmt"
func process(n int) error {
err := validate(n)
if err != nil {
return fmt.Errorf("process: %w", err)
}
return nil
}
%w именно оборачивает ошибку: новый объект содержит текст "process: число должно быть положительным", но внутри хранит ссылку на исходный ErrNotPositive. Это не то же самое, что fmt.Sprintf — у него нет %w, и цепочка была бы потеряна.
Теперь вопрос: как вызывающий код определит, что внутри обёртки именно ErrNotPositive, а не другая ошибка? Прямое сравнение err == ErrNotPositive не пройдёт — err уже другой объект.
err := process(-1)
fmt.Println(err == ErrNotPositive) // false — это обёртка
fmt.Println(errors.Is(err, ErrNotPositive)) // true — разворачивает цепочку
errors.Is(err, target) проходит по всей цепочке обёрток и возвращает true, как только находит target. Цепочка может быть сколь угодно длинной — errors.Is доберётся до корня.
Практический вывод: errors.New создаёт «якорь», fmt.Errorf %w добавляет контекст на каждом уровне, errors.Is находит якорь в любой глубине обёртки.
Проверка ошибки через if err != nil и функция validatePositive
После вызова функции, возвращающей error, принято проверять результат немедленно и завершать ветку с ошибкой явно — через return, return err или передачу выше:
result, err := someFunc()
if err != nil {
return err // или log + return, или return 0, err
}
// только здесь result можно использовать согласно контракту функции
Ключ — в завершении ветки. Если после if err != nil { ... } нет return или другого выхода, выполнение продолжится с тем же result. Что именно содержит result при ошибке — зависит от контракта конкретной функции: одни возвращают нулевое значение, другие — частичный результат (например, io.Reader.Read сообщает, сколько байт успело прочитаться до сбоя). Считать result пригодным без явного разрешения контракта нельзя.
Соберём полный пример:
package main
import (
"errors"
"fmt"
)
var ErrNotPositive = errors.New("число должно быть положительным")
func validatePositive(n int) (int, error) {
if n <= 0 {
return 0, ErrNotPositive
}
return n, nil
}
func process(n int) (int, error) {
result, err := validatePositive(n)
if err != nil {
return 0, fmt.Errorf("process: %w", err)
}
return result, nil
}
func main() {
var n int
fmt.Scan(&n)
result, err := process(n)
if err != nil {
fmt.Println("ошибка:", err)
if errors.Is(err, ErrNotPositive) {
fmt.Println("причина: число не положительное")
}
return
}
fmt.Println("результат:", result)
}
Запустим с вводом -3:
ошибка: process: число должно быть положительным причина: число не положительное
Несколько деталей, на которые стоит обратить внимание.
validatePositive и process возвращают 0 в позиции int при ошибке — не потому что 0 что-то означает, а просто как неиспользуемое значение: такой контракт выбран явно. Именно поэтому process, увидев err != nil от validatePositive, немедленно возвращает 0, fmt.Errorf(...) — значение результата отбрасывается, ошибка с контекстом поднимается выше.
errors.Is внутри main находит исходный ErrNotPositive даже сквозь обёртку "process: " — этот механизм разобран в предыдущей секции, здесь мы видим его в действии целиком.
Теперь о том, что происходит при пропуске проверки:
result, _ := process(-3) // ошибка выброшена
fmt.Println(result) // печатает 0 — но это нулевое значение по контракту process при сбое, не результат вычисления
Компилятор не запретит такой код. result равен 0 не потому что вычисление дало ноль, а потому что произошёл сбой — и process намеренно вернула 0 по своему контракту. Никакого предупреждения нет — ошибка растворилась. Поэтому _ для ошибки допустим только когда вы точно знаете, что сбой в данном месте невозможен или заведомо несущественен.
Попробуйте решить
Какой фрагмент правильно обрабатывает результат функции func validatePositive(n int) (int, error) и не использует значение при ошибке?
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
