multierr для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

multierr для Go: объединение и работа с несколькими ошибками

В Go обработка ошибок — это явная и неотъемлемая часть разработки. Однако часто возникает ситуация, когда в ходе выполнения одной операции может произойти не одна, а сразу несколько ошибок. Например, при пакетной обработке файлов, валидации сложной структуры данных или выполнении параллельных задач. Стандартный пакет errors позволяет работать только с одной ошибкой за раз, что усложняет сбор и анализ всех произошедших сбоев.

Библиотека multierr от компании Uber (да, той самой, что создала zap и fx) решает эту проблему элегантно и эффективно. Она предоставляет простой API для объединения нескольких независимых ошибок в одну составную (composite) ошибку. Это позволяет сохранить контекст всех сбоев, не теряя информации, и обработать их в одном месте. В отличие от ручного создания срезов ошибок, multierr предлагает удобные функции для комбинирования, проверки наличия ошибок и итерации по ним.

Основная философия библиотеки — простота и производительность. Она не создает лишних аллокаций в куче, если в этом нет необходимости, и старается минимизировать накладные расходы. multierr идеально подходит для случаев, когда вам нужно собрать ошибки из разных источников (например, из горутин) и вернуть их как единый результат, который можно будет проверить на наличие ошибок с помощью стандартного if err != nil.

Установка

Установка библиотеки производится стандартной командой go get. Убедитесь, что у вас установлен Go версии 1.13 или выше (для поддержки errors.Is и errors.As).

go get go.uber.org/multierr

После выполнения команды библиотека будет добавлена в ваш go.mod файл. Импортируется она следующим образом:

import "go.uber.org/multierr"

Быстрый старт: минимальный пример

Давайте рассмотрим самый простой случай: у нас есть две функции, которые могут вернуть ошибку. Мы хотим выполнить обе и, если хотя бы одна из них завершилась неудачей, вернуть общую ошибку.

package main

import (
    "errors"
    "fmt"
    "go.uber.org/multierr"
)

func doSomething() error {
    return errors.New("первая ошибка")
}

func doAnotherThing() error {
    return errors.New("вторая ошибка")
}

func main() {
    var err error
    
    // Выполняем первую операцию
    err = multierr.Append(err, doSomething())
    // Выполняем вторую операцию
    err = multierr.Append(err, doAnotherThing())
    
    if err != nil {
        fmt.Println("Произошли ошибки:")
        // Выводим все ошибки, разделенные переводом строки
        for _, e := range multierr.Errors(err) {
            fmt.Println("-", e)
        }
    }
}

В этом примере multierr.Append добавляет новую ошибку к существующей. Если err равен nil, функция просто возвращает новую ошибку. В результате мы получаем одну составную ошибку, которую можно разобрать на части с помощью multierr.Errors.

Основные функции и методы

Библиотека multierr предоставляет небольшой, но очень мощный набор функций. Рассмотрим каждую из них подробно.

1. multierr.Append

Сигнатура: func Append(left error, right error) error

Описание: Это основная функция для объединения двух ошибок. Если обе ошибки равны nil, возвращается nil. Если одна из них nil, возвращается другая. Если обе не nil, они объединяются в одну составную ошибку.

err1 := errors.New("ошибка 1")
err2 := errors.New("ошибка 2")

combined := multierr.Append(err1, err2)
fmt.Println(combined) // Вывод: ошибка 1; ошибка 2

// Если одна из ошибок nil
combined2 := multierr.Append(err1, nil)
fmt.Println(combined2 == err1) // Вывод: true

2. multierr.AppendInto

Сигнатура: func AppendInto(dst *error, err error) (appended bool)

Описание: Удобная функция для накопления ошибок. Она добавляет ошибку err в указатель на ошибку dst. Возвращает true, если ошибка была добавлена (т.е. err != nil). Это позволяет писать чистый код без явного присваивания результата.

var err error

// Вместо: err = multierr.Append(err, doSomething())
appended := multierr.AppendInto(&err, doSomething())
if appended {
    fmt.Println("Ошибка была добавлена")
}

// Теперь err содержит все накопленные ошибки

3. multierr.Combine

Сигнатура: func Combine(errors ...error) error

Описание: Создает одну составную ошибку из произвольного количества ошибок (вариативная функция). Если все переданные ошибки равны nil, возвращается nil. Это самый лаконичный способ объединить несколько ошибок сразу.

errs := []error{
    errors.New("ошибка A"),
    nil,
    errors.New("ошибка B"),
}

combined := multierr.Combine(errs...)
fmt.Println(combined) // Вывод: ошибка A; ошибка B

4. multierr.Errors

Сигнатура: func Errors(err error) []error

Описание: Разбирает составную ошибку на срез отдельных ошибок. Если переданная ошибка не является составной (или равна nil), возвращается срез из одного элемента (или пустой срез). Это основной способ итерации по всем ошибкам.

err := multierr.Combine(
    errors.New("первая"),
    errors.New("вторая"),
    errors.New("третья"),
)

for i, e := range multierr.Errors(err) {
    fmt.Printf("Ошибка %d: %v\n", i+1, e)
}
// Вывод:
// Ошибка 1: первая
// Ошибка 2: вторая
// Ошибка 3: третья

5. multierr.Error (метод интерфейса)

Сигнатура: func (e *multiError) Error() string

Описание: Реализует стандартный интерфейс error. Возвращает строковое представление всех ошибок, разделенных точкой с запятой. Этот метод вызывается автоматически, когда вы передаете составную ошибку в fmt.Println или log.Fatal.

err := multierr.Append(
    errors.New("ошибка 1"),
    errors.New("ошибка 2"),
)
fmt.Println(err.Error()) // Вывод: ошибка 1; ошибка 2

6. Поддержка errors.Is и errors.As

Сигнатура: func (e *multiError) Is(target error) bool и func (e *multiError) As(target interface{}) bool

Описание: Составная ошибка от multierr полностью совместима со стандартными функциями errors.Is и errors.As. Is проверяет, содержит ли составная ошибка хотя бы одну ошибку, равную целевой. As находит первую ошибку, которая может быть приведена к целевому типу. Это позволяет использовать мощные паттерны обработки ошибок.

type MyCustomError struct {
    Code int
    Msg  string
}
func (e MyCustomError) Error() string { return e.Msg }

var targetErr MyCustomError = MyCustomError{Code: 404}

err := multierr.Combine(
    errors.New("простая ошибка"),
    targetErr,
)

// Проверяем, есть ли среди ошибок targetErr
if errors.Is(err, targetErr) {
    fmt.Println("Найдена ошибка 404!")
}

// Пытаемся извлечь ошибку определенного типа
var found MyCustomError
if errors.As(err, &found) {
    fmt.Printf("Извлечена ошибка: %+v\n", found)
}

7. multierr.Inspect

Сигнатура: func Inspect(err error) (errors []error, ok bool)

Описание: Позволяет "заглянуть" внутрь ошибки. Если ошибка является составной (созданной multierr), она возвращает срез ошибок и true. Если нет — возвращает nil и false. Полезно, когда нужно проверить, является ли ошибка составной, не разбирая ее полностью.

err := multierr.Combine(errors.New("test"))
if errs, ok := multierr.Inspect(err); ok {
    fmt.Println("Это составная ошибка, количество:", len(errs))
} else {
    fmt.Println("Это простая ошибка")
}

Реальный практический пример

Представьте, что мы пишем сервис для обработки заказов. Каждый заказ содержит список товаров. Нам нужно проверить наличие каждого товара на складе, зарезервировать его и обновить статус заказа. Если на любом из этапов произошла ошибка, мы должны собрать все ошибки и вернуть их клиенту, не прерывая обработку других товаров.

package main

import (
    "errors"
    "fmt"
    "math/rand"
    "time"
    "go.uber.org/multierr"
)

// Order представляет заказ
type Order struct {
    ID     string
    Items  []string
}

// Warehouse представляет склад
type Warehouse struct {
    Stock map[string]int
}

// NewWarehouse создает новый склад с тестовыми данными
func NewWarehouse() *Warehouse {
    return &Warehouse{
        Stock: map[string]int{
            "laptop": 10,
            "mouse":  0,
            "keyboard": 5,
            "monitor": 0,
        },
    }
}

// CheckStock проверяет наличие товара на складе
func (w *Warehouse) CheckStock(item string) error {
    if stock, exists := w.Stock[item]; !exists || stock <= 0 {
        return fmt.Errorf("товар '%s' отсутствует на складе", item)
    }
    return nil
}

// ReserveItem резервирует товар (имитация)
func (w *Warehouse) ReserveItem(item string) error {
    // Имитируем случайную ошибку резервирования
    if rand.Intn(10) < 2 { // 20% шанс ошибки
        return fmt.Errorf("не удалось зарезервировать товар '%s'", item)
    }
    w.Stock[item]--
    return nil
}

// ProcessOrder обрабатывает заказ, собирая все ошибки
func ProcessOrder(order Order, warehouse *Warehouse) error {
    var err error

    for _, item := range order.Items {
        // Шаг 1: Проверка наличия
        if checkErr := warehouse.CheckStock(item); checkErr != nil {
            err = multierr.Append(err, checkErr)
            continue // Пропускаем резервирование, если товара нет
        }

        // Шаг 2: Резервирование
        if reserveErr := warehouse.ReserveItem(item); reserveErr != nil {
            err = multierr.Append(err, reserveErr)
        }
    }

    return err
}

// SendNotification имитирует отправку уведомления об ошибках
func SendNotification(err error) {
    if err == nil {
        fmt.Println("Заказ успешно обработан!")
        return
    }

    fmt.Println("При обработке заказа произошли следующие ошибки:")
    for _, e := range multierr.Errors(err) {
        fmt.Printf("  - %v\n", e)
    }
}

func main() {
    rand.Seed(time.Now().UnixNano())

    warehouse := NewWarehouse()
    
    // Создаем тестовые заказы
    orders := []Order{
        {ID: "ORD-001", Items: []string{"laptop", "mouse", "keyboard"}},
        {ID: "ORD-002", Items: []string{"monitor", "mouse"}},
        {ID: "ORD-003", Items: []string{"laptop", "tablet"}}, // tablet нет на складе
    }

    for _, order := range orders {
        fmt.Printf("\n=== Обработка заказа %s ===\n", order.ID)
        
        // Обрабатываем заказ
        err := ProcessOrder(order, warehouse)
        
        // Отправляем уведомление (в реальном приложении это может быть HTTP ответ)
        SendNotification(err)
        
        // Проверяем, есть ли среди ошибок конкретная проблема
        var targetErr error = errors.New("товар 'tablet' отсутствует на складе")
        if errors.Is(err, targetErr) {
            fmt.Println("ВНИМАНИЕ: Обнаружена критическая ошибка с товаром 'tablet'!")
        }
    }

    // Демонстрация работы с каналами и горутинами
    fmt.Println("\n=== Параллельная обработка с multierr ===")
    errChan := make(chan error, len(orders))
    
    for _, order := range orders {
        go func(o Order) {
            errChan <- ProcessOrder(o, warehouse)
        }(order)
    }

    var combinedErr error
    for i := 0; i < len(orders); i++ {
        err := <-errChan
        combinedErr = multierr.Append(combinedErr, err)
    }

    if combinedErr != nil {
        fmt.Println("Итоговые ошибки после параллельной обработки:")
        for _, e := range multierr.Errors(combinedErr) {
            fmt.Printf("  - %v\n                    

Рекомендаций