Коротко: в Go ошибка — обычное значение, а не исключение: функция возвращает error, и вы обрабатываете его сразу; никаких try/catch.
Главное, что нужно понять про ошибки в Go: ошибка — это обычное значение, а не исключение. Функция возвращает error вторым результатом, и вы обрабатываете его сразу. Никаких try/catch в обычном потоке. Ниже — как создавать ошибки, оборачивать их с сохранением причины, разбирать через errors.Is/errors.As и когда уместны panic/recover.
Ошибки — это значения
Базовый паттерн Go: функция возвращает результат и error. Если ошибка не nil — что-то пошло не так:
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("чтение конфига: %w", err)
}
// здесь data валидна
error — это просто интерфейс с одним методом Error() string. Любой тип, реализующий его, является ошибкой.
Создание ошибок
Два базовых способа — errors.New для простого текста и fmt.Errorf для форматирования:
import "errors"
err1 := errors.New("пользователь не найден")
err2 := fmt.Errorf("не удалось обработать заказ %d", orderID)
Обёртывание ошибок (%w)
Чтобы не терять исходную причину при проброс ошибки вверх, её оборачивают глаголом %w. Так формируется цепочка ошибок с контекстом на каждом уровне:
func loadUser(id int) (*User, error) {
row, err := db.Query(id)
if err != nil {
return nil, fmt.Errorf("loadUser(%d): %w", id, err)
}
// ...
}
В отличие от %v (просто текст), %w сохраняет саму ошибку внутри — её потом можно достать.
Разбор ошибок: errors.Is и errors.As
errors.Is проверяет, есть ли в цепочке конкретная ошибка (sentinel), а errors.As — достаёт ошибку нужного типа:
// sentinel-ошибка пакета
var ErrNotFound = errors.New("не найдено")
func get(id int) error {
return fmt.Errorf("get %d: %w", id, ErrNotFound)
}
func main() {
err := get(42)
if errors.Is(err, ErrNotFound) {
fmt.Println("это ошибка 'не найдено'")
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("проблема с путём:", pathErr.Path)
}
}
Это правильная замена сравнению строк ошибок — оно ломается при любом изменении текста.
Кастомные типы ошибок
Когда нужно передать вместе с ошибкой данные (код, поле, причину), создают свой тип:
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("поле %q: %s", e.Field, e.Msg)
}
// использование
return &ValidationError{Field: "email", Msg: "некорректный формат"}
Такой тип потом удобно ловить через errors.As.
panic и recover — для исключительных ситуаций
panic разворачивает стек и аварийно завершает программу. Это не механизм бизнес-логики — его применяют для действительно непредвиденного (нарушение инварианта, баг). Перехватить панику можно через recover внутри defer:
func safeRun() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("паника перехвачена: %v", r)
}
}()
doRiskyThing() // если внутри panic — поймаем
return nil
}
Типичный уместный случай — recover на верхнем уровне HTTP-обработчика, чтобы одна паника не уронила весь сервер.
defer для освобождения ресурсов
defer откладывает вызов до выхода из функции — идеально для закрытия ресурсов, чтобы не забыть про них при раннем return по ошибке:
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // выполнится при любом выходе из функции
Хорошие практики
- Обрабатывайте ошибку сразу после вызова, не копите.
- Добавляйте контекст через
%wна каждом значимом уровне, но без дублирования. - Сравнивайте ошибки через
errors.Is/errors.As, а не по тексту. - Не используйте
panicдля ожидаемых ошибок (пустой ввод, не найдено и т.п.). - Не игнорируйте
errчерез_, если не уверены на 100%, что он не важен.
Явная обработка ошибок поначалу кажется многословной, но именно она делает Go-код предсказуемым и устойчивым в проде. Если переносите на Go сервис с большим количеством внешних интеграций и думаете об архитектуре обработки сбоев — обсудим на консультации.


Комментарии