В большинстве языков ошибки — это исключения: что-то ломается, выполнение «выпрыгивает» из функции, и где-то выше его ловит try/catch. В Go всё устроено иначе. Здесь ошибка — это обычное значение, которое функция возвращает наравне с результатом. Разберём, почему так сделано и как с этим жить.
Зачем ошибки как значенияспросят на собеседовании
Главная идея Go: ошибка — это часть результата функции, а не отдельный поток управления. Функция, которая может не справиться, возвращает два значения — результат и error:
func ReadConfig(path string) (Config, error) {
// ...
}
Вызывающий код видит второе значение прямо перед собой как переменную, и компилятор не даст объявить err и забыть про него. Полной страховки всё же нет: вызов без присваивания (os.Remove(path)) или _ вместо err молча проглотят ошибку — такие места ловит линтер errcheck, а в ревью на них смотрят отдельно. Минус — проверок становится много. Плюс — поток выполнения линеен и предсказуем: ты всегда видишь, где именно что-то может пойти не так.
Короткая формула: в Go не «бросают» ошибку, её возвращают и тут же проверяют.
Что такое errorспросят на собеседовании
error — это встроенный interface с единственным методом:
type error interface {
Error() string
}
Любой тип, у которого есть метод Error() string, является ошибкой. Это значит, что ошибка — не магия, а обычное значение какого-то типа. Если ошибки нет, функция возвращает nil на месте error.
value, err := strconv.Atoi("42") // превращает строку в число
if err != nil {
// сюда не зайдём: "42" корректно
}
// value == 42
Подробнее про интерфейсы — в отдельной статье (ссылка ниже); здесь достаточно знать, что error — это контракт «я умею рассказать о себе строкой».
Идиома if err != nil
Базовый приём, который ты будешь писать постоянно:
f, err := os.Open("data.txt")
if err != nil {
return err // не справились — сообщаем выше
}
// дальше работаем с f, зная, что ошибки нет
Несколько правил, которые делают этот код идиоматичным:
- Проверяй ошибку сразу после вызова, до использования результата.
- Если обработать ошибку здесь нельзя — возвращай её выше (
return err), пусть разбирается тот, кто знает контекст. - При ошибке остальные возвращаемые значения обычно бессмысленны — не используй их.
// типичная цепочка: каждый шаг может сорваться
data, err := os.ReadFile(path)
if err != nil {
return err
}
cfg, err := parse(data)
if err != nil {
return err
}
return validate(cfg)
Да, проверок много. Это осознанный выбор языка: явность важнее краткости.
Как создавать ошибки
Два основных способа сделать ошибку.
errors.New — простая ошибка с фиксированным текстом:
import "errors"
var ErrNotFound = errors.New("запись не найдена")
func find(id int) (Item, error) {
// ...
return Item{}, ErrNotFound
}
Ошибку часто кладут в переменную с префиксом Err (так называемая sentinel-ошибка) — чтобы выше её можно было сравнить и принять решение.
fmt.Errorf — когда в текст нужно подставить данные:
import "fmt"
func find(id int) (Item, error) {
// ...
return Item{}, fmt.Errorf("запись id=%d не найдена", id)
}
fmt.Errorf форматирует строку так же, как fmt.Printf, но возвращает error.
Обёртывание и errors.Is / errors.Asспросят на собеседовании
Когда ошибка поднимается через несколько слоёв, голый текст вроде "запись не найдена" не помогает: непонятно, где и при чём это случилось. Решение — обёртывание (wrapping): добавить контекст, но сохранить исходную ошибку внутри.
Для этого в fmt.Errorf используется глагол %w (от wrap):
func loadUser(id int) (User, error) {
u, err := repo.find(id)
if err != nil {
// добавляем контекст, но не теряем исходную ошибку
return User{}, fmt.Errorf("loadUser id=%d: %w", id, err)
}
return u, nil
}
Теперь текст ошибки накапливает «маршрут» (loadUser id=7: запись не найдена), а внутри по-прежнему лежит оригинал. Достать его помогают две функции.
errors.Is — проверить, что где-то в цепочке обёрток есть конкретная sentinel-ошибка:
_, err := loadUser(7)
if errors.Is(err, ErrNotFound) {
// да, в глубине это именно "не найдено" — отвечаем соответствующе
}
Сравнивать через == здесь нельзя: обёрнутая ошибка — уже другое значение. errors.Is разворачивает слои за тебя.
errors.As — когда нужна не просто проверка, а доступ к полям ошибки определённого типа:
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "некорректное поле: " + e.Field
}
var vErr *ValidationError
if errors.As(err, &vErr) {
// нашли ValidationError в цепочке — можно прочитать vErr.Field
fmt.Println("проблемное поле:", vErr.Field)
}
Короткая формула: оборачивай через %w, проверяй значение через errors.Is, извлекай тип через errors.As.
panic и recover — редкий случайспросят на собеседовании
В Go есть и механизм, похожий на исключения: panic разворачивает стек и аварийно завершает программу, а recover позволяет его перехватить. Но это не замена обычным ошибкам.
panic уместен только для ситуаций «программа в принципе не может продолжать»: нарушен внутренний инвариант, некорректно собрана программа, обращение за границу slice. Для ожидаемых сбоев (файл не найден, неверный ввод) — всегда возвращай error.
func recovered() {
defer func() {
if r := recover(); r != nil {
// перехватили панику, не дали процессу упасть
fmt.Println("восстановились после:", r)
}
}()
panic("что-то пошло совсем не так")
}
recover работает только внутри defer-функции. Типичное применение — на границе, чтобы одна сбойная задача не уронила весь сервис. Правило по умолчанию: ошибки возвращай, panic не используй.
Ошибки на уровне HTTP и backend
Этот раздел — про язык. Как ошибки превращаются в HTTP-ответы (коды статусов, единый формат тела, логирование на границе обработчика) — отдельная тема backend-уровня. Об этом — в статье Ошибки и HTTP в Go из раздела про серверную разработку.
Глубже: несколько ошибок сразу, стиль обёртки и nil, который не nilрасширенноеспросят на собеседовании
Валидация формы находит не одну ошибку, а три, и терять две из них обидно. С Go 1.20 несколько ошибок собирают в одну через errors.Join, и errors.Is/errors.As умеют заглядывать в каждую:
живой пример
package main
import (
"errors"
"fmt"
)
var ErrEmptyName = errors.New("пустое имя")
var ErrBadEmail = errors.New("некорректная почта")
func validate(name, email string) error {
var errs []error
if name == "" {
errs = append(errs, ErrEmptyName)
}
if email == "" {
errs = append(errs, ErrBadEmail)
}
return errors.Join(errs...) // nil, если срез пуст
}
func main() {
err := validate("", "")
fmt.Println(err) // обе ошибки, каждая с новой строки
fmt.Println(errors.Is(err, ErrBadEmail)) // true
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Тем же способом fmt.Errorf принимает несколько %w в одной строке. Обратную операцию — снять один слой обёртки — делает errors.Unwrap, но в прикладном коде она почти не нужна: Is и As ходят по цепочке сами.
Про стиль. Текст ошибки пишут со строчной буквы и без точки в конце, потому что он станет серединой чужой фразы: loadUser id=7: запись не найдена. Контекст добавляют коротким «что делали», а не пересказом. И глагол %v вместо %w — не безобидная опечатка: текст сохранится, а цепочка оборвётся, и errors.Is выше перестанет находить исходную ошибку.
Последняя ловушка не про пакет errors, а про интерфейсы: функция, которая объявила переменную типа *MyError и вернула её пустой, отдаст не nil, а error с типом внутри — и if err != nil сработает. Разбор с примером — в интерфейсах, раздел про nil внутри интерфейса.
Коротко
- Ошибка в Go — это значение типа
error(интерфейс с методомError() string), а не исключение. - Функция возвращает
errorрядом с результатом; вызывающий проверяет его идиомойif err != nil. - Создавай ошибки через
errors.New(фиксированный текст) илиfmt.Errorf(с данными). - Оборачивай ошибку через
%w, чтобы добавить контекст и сохранить оригинал внутри. errors.Isищет в цепочке конкретную sentinel-ошибку,errors.As— извлекает ошибку нужного типа.panic/recover— только для неустранимых сбоев; для ожидаемых ошибок всегда возвращайerror.errors.Joinи несколько%wсобирают ошибки в одну; текст ошибки — со строчной буквы и без точки,%vвместо%wрвёт цепочку.
Что пощупать
В практикуме remodov/marketplace-system-go ошибки двух видов: значение ErrInvalid, которое узнают через errors.Is, и тип *OutOfStockError с числами внутри для errors.As; обработчик переводит их в 400 и 409.
Код: errors.go, handler.go.
Сделаем сами
Ветка step-03-commands-and-errors.
Что почитать дальше
- Интерфейсы в Go — почему
errorэто interface и как устроены контракты типов. - Горутины и каналы — как передавать ошибки из параллельного кода.
- Синтаксис и типы — основа, если множественный возврат и
:=ещё в новинку.