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

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

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

Начать курс

Библиотека errors для Go: полное руководство по улучшенной обработке ошибок

1. Что это и зачем нужна

Библиотека errors — это стандартный пакет языка Go, предоставляющий базовые функции для создания, форматирования и обработки ошибок. В отличие от многих других языков, где исключения (exceptions) являются основным механизмом обработки ошибок, Go использует явный возврат значений ошибок из функций. Это делает поток выполнения программы более предсказуемым и контролируемым.

Основная задача библиотеки — предоставить простой и эффективный способ создания новых ошибок, сравнения их типов и добавления контекстной информации (например, "обёртывания" ошибок). В Go ошибка — это просто интерфейс error, который реализует любой тип с методом Error() string. Это даёт огромную гибкость: вы можете создавать собственные типы ошибок с дополнительными полями (код ошибки, данные запроса и т.д.).

Зачем она нужна? Без неё невозможно написать надёжное Go-приложение. Каждый раз, когда функция может завершиться неудачно (работа с файлами, сетью, базой данных), она возвращает ошибку. Игнорирование ошибок (присвоение их переменной _) считается плохой практикой. Библиотека errors позволяет не только сообщить, что пошло не так, но и построить цепочку ошибок, чтобы при отладке точно знать, на каком этапе произошёл сбой.

2. Установка

Пакет errors является частью стандартной библиотеки Go. Это означает, что никакой дополнительной установки не требуется. Он доступен "из коробки" при установке любого Go SDK. Для его использования достаточно импортировать его в ваш код:

import "errors"

Если вы используете Go Modules (а это стандарт с Go 1.11+), то пакет будет автоматически доступен. Никаких команд go get для стандартной библиотеки выполнять не нужно.

Важно: Существует также популярная сторонняя библиотека github.com/pkg/errors, которая предоставляет расширенный функционал (стеки вызовов, форматирование). Однако в этой статье мы фокусируемся исключительно на стандартном пакете errors, который является основой для любой обработки ошибок в Go.

3. Быстрый старт

Минимальный рабочий пример создания и обработки ошибки:

package main

import (
    "errors"
    "fmt"
)

// Функция, которая делит два числа. Если делитель равен нулю, возвращает ошибку.
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("деление на ноль невозможно")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Ошибка:", err)
    } else {
        fmt.Println("Результат:", result)
    }

    // Попробуем с ошибкой
    result2, err2 := divide(10, 0)
    if err2 != nil {
        fmt.Println("Ошибка:", err2) // Вывод: Ошибка: деление на ноль невозможно
    } else {
        fmt.Println("Результат:", result2)
    }
}

В этом примере мы создали простую ошибку с помощью errors.New() и проверили её наличие стандартным способом if err != nil.

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

Ниже приведены 8 ключевых функций/методов пакета errors с подробным описанием и примерами.

4.1 errors.New(text string) error

Сигнатура: func New(text string) error

Что делает: Создаёт новую ошибку с заданным текстовым сообщением. Это самый простой способ создать ошибку. Возвращаемое значение — это указатель на внутреннюю структуру errorString, которая реализует интерфейс error.

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("что-то пошло не так")
    fmt.Println(err) // Вывод: что-то пошло не так
    fmt.Printf("Тип: %T\n", err) // Вывод: Тип: *errors.errorString
}

4.2 errors.Unwrap(err error) error

Сигнатура: func Unwrap(err error) error

Что делает: Извлекает "внутреннюю" ошибку из цепочки обёрнутых ошибок. Если ошибка не имеет метода Unwrap() (или он возвращает nil), функция возвращает nil. Это ключевая функция для работы с цепочками ошибок, созданными с помощью fmt.Errorf и директивы %w.

package main

import (
    "errors"
    "fmt"
)

func main() {
    baseErr := errors.New("базовая ошибка")
    wrappedErr := fmt.Errorf("обёрнутая ошибка: %w", baseErr)

    // Извлекаем внутреннюю ошибку
    unwrappedErr := errors.Unwrap(wrappedErr)
    fmt.Println(unwrappedErr) // Вывод: базовая ошибка

    // Попытка извлечь из необёрнутой ошибки
    noUnwrap := errors.Unwrap(baseErr)
    fmt.Println(noUnwrap) // Вывод: <nil>
}

4.3 errors.Is(err, target error) bool

Сигнатура: func Is(err, target error) bool

Что делает: Проверяет, является ли ошибка err "равной" ошибке target (или содержит её в цепочке обёртывания). Сравнение происходит по значению (через ==), но с учётом цепочки: если err обёртывает target, функция вернёт true. Это замена прямому сравнению err == target, которое не работает для обёрнутых ошибок.

package main

import (
    "errors"
    "fmt"
)

var ErrNotFound = errors.New("не найдено")

func findUser(id int) error {
    if id != 42 {
        return fmt.Errorf("пользователь %d: %w", id, ErrNotFound)
    }
    return nil
}

func main() {
    err := findUser(1)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("Пользователь не найден!") // Вывод: Пользователь не найден!
    }

    // Прямое сравнение не сработает
    if err == ErrNotFound {
        fmt.Println("Это не будет выведено")
    }
}

4.4 errors.As(err error, target interface{}) bool

Сигнатура: func As(err error, target interface{}) bool

Что делает: Ищет в цепочке ошибок первую ошибку, тип которой соответствует типу, на который указывает target (который должен быть указателем на конкретный тип ошибки). Если находит, присваивает эту ошибку переменной, на которую указывает target, и возвращает true. Это аналог type assertion для цепочек ошибок.

package main

import (
    "errors"
    "fmt"
)

// Собственный тип ошибки с дополнительным полем
type MyError struct {
    Code    int
    Message string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("код %d: %s", e.Code, e.Message)
}

func process() error {
    return &MyError{Code: 500, Message: "внутренняя ошибка сервера"}
}

func main() {
    err := process()
    
    var myErr *MyError
    if errors.As(err, &myErr) {
        fmt.Printf("Поймали ошибку! Код: %d, Сообщение: %s\n", myErr.Code, myErr.Message)
        // Вывод: Поймали ошибку! Код: 500, Сообщение: внутренняя ошибка сервера
    }
}

4.5 fmt.Errorf(format string, a ...interface{}) error

Сигнатура: func Errorf(format string, a ...interface{}) error

Что делает: Форматирует строку по правилам fmt.Sprintf и возвращает её как ошибку. Ключевая особенность — директива %w, которая позволяет обернуть существующую ошибку в новую, создавая цепочку. Без %w это просто создание новой ошибки из строки.

package main

import (
    "errors"
    "fmt"
)

func main() {
    base := errors.New("база данных недоступна")
    
    // Обёртывание ошибки с контекстом
    wrapped := fmt.Errorf("не удалось получить пользователя: %w", base)
    fmt.Println(wrapped) // Вывод: не удалось получить пользователя: база данных недоступна

    // Простое форматирование (без обёртывания)
    simple := fmt.Errorf("ошибка в строке %d", 42)
    fmt.Println(simple) // Вывод: ошибка в строке 42
}

4.6 errors.Join(errs ...error) error

Сигнатура: func Join(errs ...error) error

Что делает: (Доступно с Go 1.20) Объединяет несколько ошибок в одну. Возвращает nil, если все переданные ошибки — nil. Это полезно, когда нужно собрать все ошибки из цикла или параллельных операций. Результирующая ошибка поддерживает Unwrap() (возвращает последнюю ошибку) и Is()/As() (проверяет все вложенные ошибки).

package main

import (
    "errors"
    "fmt"
)

func main() {
    err1 := errors.New("ошибка 1")
    err2 := errors.New("ошибка 2")
    err3 := errors.New("ошибка 3")

    // Объединяем ошибки
    combined := errors.Join(err1, err2, err3)
    fmt.Println(combined) 
    // Вывод: ошибка 1
    // ошибка 2
    // ошибка 3

    // Проверка Is работает для всех вложенных ошибок
    fmt.Println(errors.Is(combined, err2)) // Вывод: true
}

4.7 errors.New(text string) error — Создание кастомных ошибок

Сигнатура: func New(text string) error (уже рассмотрена выше)

Что делает: Помимо простого создания, New часто используется для создания "сентинельных" (sentinel) ошибок — глобальных переменных-ошибок, которые служат для сравнения через errors.Is. Это стандартный паттерн в Go.

package main

import (
    "errors"
    "fmt"
)

// Сентинельные ошибки
var (
    ErrInvalidInput = errors.New("неверный ввод")
    ErrTimeout      = errors.New("таймаут операции")
)

func validate(input string) error {
    if input == "" {
        return ErrInvalidInput
    }
    return nil
}

func main() {
    err := validate("")
    if errors.Is(err, ErrInvalidInput) {
        fmt.Println("Ошибка валидации:", err)
    }
}

4.8 Реализация интерфейса error

Сигнатура: type error interface { Error() string }

Что делает: Любой тип, реализующий метод Error() string, автоматически удовлетворяет интерфейсу error. Это позволяет создавать сложные структуры ошибок с дополнительной информацией (код, данные, стек вызовов).

package main

import (
    "fmt"
)

// Структура для HTTP-ошибки
type HTTPError struct {
    StatusCode int
    Body       string
}

// Реализация интерфейса error
func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Body)
}

// Дополнительный метод
func (e *HTTPError) IsClientError() bool {
    return e.StatusCode >= 400 && e.StatusCode < 500
}

func fetch(url string) error {
    return &HTTPError{StatusCode: 404, Body: "Not Found"}
}

func main() {
    err := fetch("http://example.com/notfound")
    if httpErr, ok := err.(*HTTPError); ok {
        fmt.Printf("Статус: %d, Тело: %s\n", httpErr.StatusCode, httpErr.Body)
        if httpErr.IsClientError() {
            fmt.Println("Это ошибка клиента")
        }
    }
}

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

Рассмотрим пример сервиса для работы с пользователями, который использует все основные приёмы работы с ошибками: сентинельные ошибки, обёртывание, кастомные типы и проверку цепочек.

package main

import (
    "errors"
    "fmt"
    "log"
    "os"
    "time"
)

// --- Опреде                    

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