Уроки модуля (2/5)
Идиоматическая обработка ошибок
Обработка ошибок в Go очевидна: никаких исключений, никаких попыток/перехвата. Каждая функция, которая может завершиться неудачно, возвращает error в качестве своего последнего значения, и решение принимает вызывающая сторона. В этой главе собраны идиоматические шаблоны для надежного создания, распространения и проверки ошибок.
Золотое правило
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("config %s: %w", path, err)
}Четыре принципа:
error— последний возврат.- Немедленно проверьте с помощью
if err != nil. - Добавьте контекст перед распространением (с помощью
fmt.Errorf+%w). - Никогда не игнорируйте ошибку: либо обработайте ее, запишите в журнал, либо верните ее.
_ = file.Close() // EXPLICITLY ignored (you know what you're doing)Дозорные ошибки
Дозорная ошибка — это экспортированная глобальная переменная var ErrXxx = errors.New("..."), с которой вызывающий объект может сравнивать:
package store
var ErrNotFound = errors.New("not found")
func Get(key string) (string, error) {
if !exists(key) {
return "", ErrNotFound
}
// ...
}Вызывающий использует errors.Is (НЕ ==), который проходит по цепочке переноса:
v, err := store.Get(k)
if errors.Is(err, store.ErrNotFound) {
// handle 404
}Стандартные примеры: io.EOF, sql.ErrNoRows, os.ErrNotExist.
Пользовательские типизированные ошибки
Для переноса структурированных данных (HTTP-код, недопустимое поле и т. д.) определите тип:
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}Вызывающая сторона использует errors.As для безопасного понижения уровня по цепочке:
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Println("invalid field:", ve.Field)
}errors.As проходит по цепочке Unwrap(), пока не обнаружит ошибку, связанную с переданным указателем.
Обертывание с помощью %w
fmt.Errorf с глаголом %w создает ошибку, которая заворачивает исходную ошибку, сохраняя ее для errors.Is/errors.As:
if err := db.Query(); err != nil {
return fmt.Errorf("load users: %w", err)
}Без %w (с использованием %v или %s) вы получаете более богатую строку, но теряете цепочку: вызывающий объект больше не может распознавать определенные ошибки.
Правила для %w:
- Только один на вызов
fmt.Errorf(для многократного переноса требуетсяerrors.Join). - Аргумент ДОЛЖЕН быть
error, а не строкой. – Добавьте контекст, не дублируйте исходное сообщение.
// bad: duplicates
return fmt.Errorf("error: %w", err) // "error: file not found"
// good: useful context
return fmt.Errorf("load config %s: %w", path, err)errors.Join для нескольких ошибок
Начиная с Go 1.20, errors.Join(errs...) объединяет несколько ошибок в одну, благодаря чему их все можно обнаружить с помощью errors.Is/As:
var errs []error
for _, item := range items {
if err := process(item); err != nil {
errs = append(errs, fmt.Errorf("item %s: %w", item.ID, err))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}Когда использовать panic
Никогда для ожидаемых ошибок. Только в трёх случаях:
- Неустранимая ошибка: инвариант нарушен в месте, которое «не может произойти» (
panic("unreachable")). - Сбой инициализации в
init()/main()автономной программы (отсутствует критическая конфигурация → быстрый сбой). - Библиотечный API, который явно документирует это: например,
regexp.MustCompile(паника, если шаблон недействителен — приемлемо, поскольку шаблон является константой времени сборки).
Все остальное error. Не используйте panic в качестве ярлыка для распространения вверх: это нарушает композицию, пропускает defer Close() и перегружает вызывающую программу.
Упражнения
Оберните ошибку os.ReadFile с fmt.Errorf, используя слово %w и добавляя соревнование 'config:' (с путем).
Решение доступно после 3 попыток
Ошибки США. Для проверки есть ошибка в катене ди обертки, которая соответствует всем дозорным ErrNotFound и штампу «manca!» в таль случай.
Решение доступно после 3 попыток
Когда идиоматически используется паника в библиотеке. Идти?
func F(x int) {
// ??? panic(...)
}