Easy📖Теория9 min

Обработка ошибок

Интерфейс error, создание и оборачивание ошибок, errors.Is/As, пользовательские типы ошибок, panic/recover

Обработка ошибок

Обработка ошибок — одна из самых характерных особенностей Go. Вместо исключений (exceptions) Go использует явный возврат ошибок как значений.

"Errors are values." — Rob Pike

Интерфейс error

В Go ошибка — это любое значение, реализующее интерфейс error:

// Built-in error interface
type error interface {
    Error() string
}

Это один из самых маленьких и при этом самых важных интерфейсов в Go.

Базовая обработка ошибок

package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    // The fundamental Go error handling pattern
    f, err := os.Open("config.json")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer f.Close()

    // Type conversion with error
    n, err := strconv.Atoi("not-a-number")
    if err != nil {
        fmt.Println("Conversion error:", err)
        // Handle error, don't just log and continue
    }
    _ = n
}

Идиома Go: Проверяйте ошибку сразу после вызова. Счастливый путь (happy path) должен идти с минимальным отступом.

// GOOD: happy path with minimal indentation
func process() error {
    data, err := fetchData()
    if err != nil {
        return fmt.Errorf("fetch data: %w", err)
    }

    result, err := transform(data)
    if err != nil {
        return fmt.Errorf("transform: %w", err)
    }

    if err := save(result); err != nil {
        return fmt.Errorf("save: %w", err)
    }

    return nil
}

// BAD: deeply nested happy path
func processBad() error {
    data, err := fetchData()
    if err == nil {
        result, err := transform(data)
        if err == nil {
            err := save(result)
            if err == nil {
                return nil
            }
            return err
        }
        return err
    }
    return err
}

Создание ошибок

errors.New — простая ошибка

package main

import (
    "errors"
    "fmt"
)

// Simple error with a static message
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", ErrNotFound
    }
    return "Alice", nil
}

func main() {
    user, err := findUser(-1)
    if err != nil {
        fmt.Println(err) // "not found"
        return
    }
    fmt.Println(user)
}

fmt.Errorf — форматированные ошибки

package main

import "fmt"

func validateAge(age int) error {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be non-negative)", age)
    }
    if age > 150 {
        return fmt.Errorf("invalid age: %d (unrealistic value)", age)
    }
    return nil
}

func main() {
    if err := validateAge(-5); err != nil {
        fmt.Println(err) // "invalid age: -5 (must be non-negative)"
    }
}

Оборачивание ошибок (Error Wrapping)

Начиная с Go 1.13, глагол %w в fmt.Errorf позволяет оборачивать ошибки, сохраняя цепочку причин:

package main

import (
    "errors"
    "fmt"
    "os"
)

var ErrConfigNotFound = errors.New("config not found")

func readConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        // Wrap the original error with context
        return nil, fmt.Errorf("read config %s: %w", path, err)
    }
    return data, nil
}

func loadApp() error {
    _, err := readConfig("/etc/myapp/config.yaml")
    if err != nil {
        // Wrap again — building an error chain
        return fmt.Errorf("load app: %w", err)
    }
    return nil
}

func main() {
    err := loadApp()
    if err != nil {
        fmt.Println(err)
        // Output: load app: read config /etc/myapp/config.yaml:
        //         open /etc/myapp/config.yaml: no such file or directory

        // Check if the error chain contains a specific error
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("File does not exist!")
        }
    }
}

%w vs %v/%s

Формат Оборачивает? errors.Is/As работает? Пример
%w Да Да fmt.Errorf("wrap: %w", err)
%v Нет Нет fmt.Errorf("context: %v", err)
%s Нет Нет fmt.Errorf("context: %s", err)

Правило: Используйте %w, когда вызывающий код должен иметь возможность проверить оригинальную ошибку. Используйте %v, когда хотите скрыть детали реализации.

Множественное оборачивание (Go 1.20+)

// Go 1.20+: wrap multiple errors
func validateForm(name, email string) error {
    var errs []error

    if name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if email == "" {
        errs = append(errs, errors.New("email is required"))
    }

    if len(errs) > 0 {
        return errors.Join(errs...)
    }
    return nil
}

// fmt.Errorf can wrap multiple errors too (Go 1.20+)
func process() error {
    err1 := step1()
    err2 := step2()
    if err1 != nil || err2 != nil {
        return fmt.Errorf("process failed: %w, %w", err1, err2)
    }
    return nil
}

errors.Is, errors.As, errors.Unwrap

errors.Is — проверка по значению

package main

import (
    "errors"
    "fmt"
    "io"
    "os"
)

var (
    ErrNotFound   = errors.New("not found")
    ErrForbidden  = errors.New("forbidden")
)

func findItem(id int) error {
    // Wrapping ErrNotFound
    return fmt.Errorf("find item %d: %w", id, ErrNotFound)
}

func main() {
    err := findItem(42)

    // errors.Is traverses the ENTIRE error chain
    if errors.Is(err, ErrNotFound) {
        fmt.Println("Item not found") // This prints!
    }

    // Works with standard library sentinel errors too
    _, err = os.Open("nonexistent.txt")
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("File does not exist")
    }

    // Check for io.EOF
    if errors.Is(err, io.EOF) {
        fmt.Println("End of file")
    }

    // Direct comparison (== ) only checks the TOP-level error
    if err == ErrNotFound {
        // This does NOT work for wrapped errors!
    }
}

errors.As — проверка по типу

package main

import (
    "errors"
    "fmt"
    "net"
    "os"
)

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: field %q — %s", e.Field, e.Message)
}

func validateUser(name string) error {
    if name == "" {
        return fmt.Errorf("validate user: %w", &ValidationError{
            Field:   "name",
            Message: "cannot be empty",
        })
    }
    return nil
}

func main() {
    err := validateUser("")

    // errors.As extracts the error of a specific TYPE from the chain
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("Field: %s, Message: %s\n", valErr.Field, valErr.Message)
        // Field: name, Message: cannot be empty
    }

    // Works with standard library error types
    _, err = net.Dial("tcp", "invalid:address")
    var netErr *net.OpError
    if errors.As(err, &netErr) {
        fmt.Printf("Network error: op=%s, net=%s\n", netErr.Op, netErr.Net)
    }

    // os.PathError example
    _, err = os.Open("nonexistent.txt")
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Printf("Path error: op=%s, path=%s\n", pathErr.Op, pathErr.Path)
    }
}

errors.Unwrap

func main() {
    inner := errors.New("database connection failed")
    wrapped := fmt.Errorf("user service: %w", inner)
    outer := fmt.Errorf("API handler: %w", wrapped)

    // Unwrap one level at a time
    fmt.Println(errors.Unwrap(outer))  // "user service: database connection failed"
    fmt.Println(errors.Unwrap(wrapped)) // "database connection failed"
    fmt.Println(errors.Unwrap(inner))   // nil (no wrapped error)
}

Пользовательские типы ошибок

Ошибка с дополнительными данными

package main

import (
    "fmt"
    "time"
)

// Custom error with HTTP status code
type HTTPError struct {
    StatusCode int
    Message    string
    URL        string
    Timestamp  time.Time
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("HTTP %d: %s (url: %s)", e.StatusCode, e.Message, e.URL)
}

// Implement Is() for custom equality comparison
func (e *HTTPError) Is(target error) bool {
    t, ok := target.(*HTTPError)
    if !ok {
        return false
    }
    return e.StatusCode == t.StatusCode
}

// Predefined sentinel-like typed errors
var (
    ErrHTTPNotFound     = &HTTPError{StatusCode: 404, Message: "Not Found"}
    ErrHTTPUnauthorized = &HTTPError{StatusCode: 401, Message: "Unauthorized"}
)

func fetchResource(url string) error {
    return &HTTPError{
        StatusCode: 404,
        Message:    "Resource not found",
        URL:        url,
        Timestamp:  time.Now(),
    }
}

func main() {
    err := fetchResource("/api/users/999")

    // errors.Is works because we implemented Is()
    if errors.Is(err, ErrHTTPNotFound) {
        fmt.Println("Resource not found!")
    }

    // errors.As to access fields
    var httpErr *HTTPError
    if errors.As(err, &httpErr) {
        fmt.Printf("Status: %d, URL: %s\n", httpErr.StatusCode, httpErr.URL)
    }
}

Ошибка с методом Unwrap

type ServiceError struct {
    Service string
    Op      string
    Err     error // Wrapped error
}

func (e *ServiceError) Error() string {
    return fmt.Sprintf("%s.%s: %v", e.Service, e.Op, e.Err)
}

// Unwrap returns the wrapped error
func (e *ServiceError) Unwrap() error {
    return e.Err
}

func getUser(id int) error {
    dbErr := fmt.Errorf("connection refused")
    return &ServiceError{
        Service: "UserService",
        Op:      "GetByID",
        Err:     dbErr,
    }
}

Sentinel Errors

Sentinel errors — предопределённые ошибки-значения для известных условий:

package mypackage

import "errors"

// Sentinel errors — package-level variables
var (
    ErrNotFound     = errors.New("not found")
    ErrAlreadyExists = errors.New("already exists")
    ErrInvalidInput = errors.New("invalid input")
    ErrUnauthorized = errors.New("unauthorized")
    ErrForbidden    = errors.New("forbidden")
    ErrTimeout      = errors.New("operation timed out")
)

// Usage in the package
func Find(id string) (*Item, error) {
    item, ok := store[id]
    if !ok {
        return nil, ErrNotFound
    }
    return item, nil
}

// Usage by consumers
func main() {
    item, err := mypackage.Find("abc")
    if errors.Is(err, mypackage.ErrNotFound) {
        // Handle not found case
    }
}

Стандартная библиотека содержит множество sentinel errors:

Пакет Ошибка Описание
io io.EOF Конец файла/потока
io io.ErrUnexpectedEOF Неожиданный конец
os os.ErrNotExist Файл не существует
os os.ErrPermission Недостаточно прав
os os.ErrExist Файл уже существует
context context.Canceled Контекст отменён
context context.DeadlineExceeded Тайм-аут
sql sql.ErrNoRows Нет результатов
net/http http.ErrServerClosed Сервер закрыт

panic и recover

panic — аварийная остановка

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

package main

import "fmt"

func main() {
    fmt.Println("Start")

    // panic stops normal execution
    // All deferred functions will still execute
    defer fmt.Println("Deferred (will execute even after panic)")

    panic("something went terribly wrong")

    fmt.Println("This will never execute")
}

// Output:
// Start
// Deferred (will execute even after panic)
// panic: something went terribly wrong
// ... stack trace ...

Когда использовать panic

// OK: Initialization that cannot fail
func MustCompileRegex(pattern string) *regexp.Regexp {
    re, err := regexp.Compile(pattern)
    if err != nil {
        panic(fmt.Sprintf("invalid regex %q: %v", pattern, err))
    }
    return re
}

// OK: Programming error (invalid arguments)
func divide(a, b int) int {
    if b == 0 {
        panic("divide: division by zero") // Bug in the caller
    }
    return a / b
}

// OK: Unreachable code
func dayName(d int) string {
    switch d {
    case 0: return "Sunday"
    case 1: return "Monday"
    // ... all cases covered
    case 6: return "Saturday"
    default:
        panic(fmt.Sprintf("invalid day: %d", d))
    }
}

recover — перехват паники

recover перехватывает панику и позволяет программе продолжить работу. Работает только внутри deferred-функции.

package main

import "fmt"

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered from panic: %v", r)
        }
    }()

    return a / b, nil
}

func main() {
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("Error:", err) // "recovered from panic: runtime error: integer divide by zero"
    } else {
        fmt.Println("Result:", result)
    }
}

Практический паттерн: HTTP middleware с recover

package main

import (
    "fmt"
    "log"
    "net/http"
    "runtime/debug"
)

// Recovery middleware prevents server crash from panics
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // Log the stack trace
                log.Printf("PANIC: %v\n%s", err, debug.Stack())

                // Return 500 to the client
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()

        next.ServeHTTP(w, r)
    })
}

panic vs error — когда что использовать

Ситуация Используй Пример
Файл не найден error os.Open() возвращает error
Сеть недоступна error http.Get() возвращает error
nil pointer dereference panic (автоматически) Runtime panic
Index out of range panic (автоматически) Runtime panic
Невалидные аргументы функции panic Ошибка программиста
Инициализация с Must* panic regexp.MustCompile()
Бизнес-логика error Всегда error

Золотое правило: Если ошибка может произойти в нормальном потоке выполнения — верните error. Если ошибка означает баг в коде — используйте panic.

Лучшие практики обработки ошибок

1. Добавляйте контекст при оборачивании

// GOOD: each level adds meaningful context
func (s *UserService) GetByID(ctx context.Context, id int) (*User, error) {
    user, err := s.repo.Find(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("UserService.GetByID(%d): %w", id, err)
    }
    return user, nil
}

// BAD: no context added
func (s *UserService) GetByIDBad(ctx context.Context, id int) (*User, error) {
    user, err := s.repo.Find(ctx, id)
    if err != nil {
        return nil, err // Caller won't know where the error came from
    }
    return user, nil
}

// BAD: redundant wrapping
func (s *UserService) GetByIDRedundant(ctx context.Context, id int) (*User, error) {
    user, err := s.repo.Find(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("error: %w", err) // "error:" adds no value
    }
    return user, nil
}

2. Обрабатывайте ошибку только один раз

// BAD: logging AND returning
func process() error {
    err := doWork()
    if err != nil {
        log.Println("Error:", err) // Logged here...
        return err                  // ...and returned for caller to log again
    }
    return nil
}

// GOOD: either log OR return, not both
func process() error {
    err := doWork()
    if err != nil {
        return fmt.Errorf("process: %w", err) // Return with context
    }
    return nil
}

// At the top level (main, HTTP handler) — log
func main() {
    if err := process(); err != nil {
        log.Fatal(err) // Log at the top level
    }
}

3. Используйте errors.Is/As вместо сравнения строк

// BAD: string comparison is fragile
if err.Error() == "not found" {
    // ...
}

// GOOD: use sentinel errors
if errors.Is(err, ErrNotFound) {
    // ...
}

// GOOD: use type assertion
var valErr *ValidationError
if errors.As(err, &valErr) {
    // ...
}

Проверь себя

🧪

Где работает `recover()`?

🧪

Какой функцией можно проверить, содержит ли цепочка ошибок конкретный тип ошибки?

🧪

Что является идиоматическим паттерном обработки ошибок в Go?

🧪

Когда уместно использовать `panic` в Go?

🧪

Чем отличается `%w` от `%v` в `fmt.Errorf`?

Связанные темы