Обработка ошибок в Go — это нечто большее, чем просто предотвращение падений программы. Она помогает строить системы, которые «ломаются красиво», ясно сообщают о проблемах и упрощают отладку. В отличие от языков в которых есть обработка исключений, Go заставляет нас явно думать о том, что может пойти не так. Это одновременно и преимущество, и недостаток.

Роб Пайк однажды сказал: «Ошибки — это значения» (прим. переводчика: В Go ошибка — это не исключение и не что‑то «особенное», а просто значение, которое может вернуть функция.) Эта простая мысль определяет то, как мы должны подходить к обработке ошибок в Go. Давайте посмотрим, как превратить эту «филосовскую» мысль в практические паттерны.

Базовый паттерн

Обработка ошибок в Go начинается с простого паттерна, который вы, наверняка видели.

func ReadConfig(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err // Return the error to the caller
    }
    return data, nil
}

// Usage
func main() {
    data, err := ReadConfig("config.json")
    if err != nil {
        log.Fatalf("Failed to read config: %v", err)
    }
    // Process data...
}

Он очевиден, но часто приводит к повторяющемуся коду. Поэтому давайте взглянем на более структурированные подходы.

Кастомные типы ошибок

Создание собственных типов ошибок позволяет давать больше контекста и обрабатывать ошибки по их типам.

// Define custom error types
type NotFoundError struct {
    Resource string
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.Resource)
}

// Function that returns our custom error
func GetUser(id string) (*User, error) {
    user, exists := userDB[id]
    if !exists {
        return nil, NotFoundError{Resource: fmt.Sprintf("User %s", id)}
    }
    return user, nil
}

// Usage with type assertion
func main() {
    user, err := GetUser("123")
    if err != nil {
        // Type-based error handling
        if notFound, ok := err.(NotFoundError); ok {
            log.Printf("Resource not available: %v", notFound)
            // Handle specifically for not found case
            return
        }
        // Handle other errors
        log.Fatalf("Unexpected error: %v", err)
    }
    // Process user...
}

Кастомные типы ошибок полезны когда вам нужно:

  • предоставлять структурированные данные об ошибке

  • давать вызывающему коду возможность принимать решения в зависимости от типа ошибки

  • группировать связанные ошибки под общим интерфейсом (прим. переводчика: не совсем понятно что тут точно имел ввиду автор в контексте кастомных типов ошибок)

Константы и переменные ошибок

В простых случаях удобны заранее определённые ошибки:

import "errors"

// Predefined error variables
var (
    ErrInvalidInput = errors.New("input is invalid")
    ErrPermissionDenied = errors.New("permission denied")
    ErrTimeout = errors.New("operation timed out")
)

func ValidateInput(input string) error {
    if len(input) < 3 {
        return ErrInvalidInput
    }
    return nil
}

// Usage with direct comparison
func main() {
    err := ValidateInput("ab")
    if err != nil {
        if errors.Is(err, ErrInvalidInput) {
            // Handle invalid input specifically
            fmt.Println("Please provide a longer input")
            return
        }
        // Handle other errors
    }
    // Continue processing...
}

Думайте о них как о кодах HTTP (прим. переводчика: например, 5xx) — они дают стандартизированный способ сообщать о конкретных ситуациях.

Обогащение ошибок контекстом

С версии Go 1.13 появилось важное нововведение — обогащение ошибок контекстом. Оно позволяет добавлять контекст, сохраняя исходную ошибку.

import (
    "errors"
    "fmt"
)

func fetchData(url string) ([]byte, error) {
    // Simulate an error
    return nil, errors.New("connection refused")
}

func processRequest(requestID string) error {
    data, err := fetchData("<https://api.example.com/data>")
    if err != nil {
        // Wrap the error with additional context
        return fmt.Errorf("processing request %s: %w", requestID, err)
    }
    // Process data...
    return nil
}

func handleRequest(requestID string) error {
    if err := processRequest(requestID); err != nil {
        // Add even more context as it bubbles up
        return fmt.Errorf("failed to handle request %s: %w", requestID, err)
    }
    return nil
}

func main() {
    err := handleRequest("REQ-123")
    if err != nil {
        // The full error chain is available
        fmt.Println(err) // Output includes all wrapped context
        
        // We can still check for the original error
        if errors.Is(err, errors.New("connection refused")) {
            fmt.Println("Network appears to be down")
        }
    }
}

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

Частые ошибки

  • Wrapping без добавления значения: return fmt.Errorf("failed: %w", err) добавляет бесполезный контекст

  • Потеря оригинальной ошибки: использование %v вместо %w. в fmt.Errorf() ломает цепочку ошибок

  • Чрезмерное количество обёрток: избыточное добавление слоев делает ошибку громоздкой

Золотое правило: оборачивайте ошибки при переходе через границы пакетов или когда добавляете полезный контекст.

Централизованное управление ошибками

С ростом сложности приложения централизованная обработка ошибок становится ключевым фактором для консистентности и дальнейшей поддержки приложения.

Middleware для ошибок

В веб-сервисах middleware обеспечивает элегантный путь для обеспечения централизованной обработки ошибок.

package main

import (
    "errors"
    "log"
    "net/http"
)

// Application error types
type AppError struct {
    Err     error
    Message string
    Code    int
}

// Handler function type that can return errors
type AppHandler func(http.ResponseWriter, *http.Request) *AppError

// Middleware that converts our AppHandler to standard http.HandlerFunc
func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        // Log the detailed error internally
        log.Printf("ERROR: %v", err.Err)
        
        // Return appropriate status code and message to client
        http.Error(w, err.Message, err.Code)
    }
}

// Example handler using our error handling pattern
func getUserHandler(w http.ResponseWriter, r *http.Request) *AppError {
    userID := r.URL.Query().Get("id")
    
    user, err := getUser(userID)
    if err != nil {
        // Check for specific error types
        var notFoundErr NotFoundError
        if errors.As(err, &notFoundErr) {
            return &AppError{
                Err:     err,
                Message: "User not found",
                Code:    http.StatusNotFound,
            }
        }
        
        // Default error response
        return &AppError{
            Err:     err,
            Message: "Internal server error",
            Code:    http.StatusInternalServerError,
        }
    }
    
    // Respond with user data...
    return nil
}

func main() {
    // Register handlers with our middleware
    http.Handle("/user", AppHandler(getUserHandler))
    http.ListenAndServe(":8080", nil)
}

Данный паттерн обеспечивает логику обработки ошибок в одном месте и гарантирует получение последовательных ответов в приложении.

Агрегация ошибок

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


import (
    "errors"
    "fmt"
    "strings"
)

// ErrorCollection aggregates multiple errors
type ErrorCollection struct {
    Errors []error
}

func (ec *ErrorCollection) Add(err error) {
    if err != nil {
        ec.Errors = append(ec.Errors, err)
    }
}

func (ec ErrorCollection) Error() string {
    if len(ec.Errors) == 0 {
        return ""
    }
    
    messages := make([]string, len(ec.Errors))
    for i, err := range ec.Errors {
        messages[i] = err.Error()
    }
    
    return fmt.Sprintf("%d errors occurred: %s", 
        len(ec.Errors), strings.Join(messages, "; "))
}

func (ec ErrorCollection) HasErrors() bool {
    return len(ec.Errors) > 0
}

// Example usage
func validateUser(user User) error {
    var errs ErrorCollection
    
    if len(user.Name) < 2 {
        errs.Add(errors.New("name too short"))
    }
    
    if len(user.Email) == 0 {
        errs.Add(errors.New("email required"))
    }
    
    if !strings.Contains(user.Email, "@") {
        errs.Add(errors.New("invalid email format"))
    }
    
    if !errs.HasErrors() {
        return nil
    }
    return errs
}

func main() {
    user := User{Name: "A", Email: "invalid-email"}
    
    if err := validateUser(user); err != nil {
        fmt.Println(err)
        // Handle validation failure
        return
    }
    
    // Continue with valid user...
}

Это особенно полезно при валидации данных: лучше сообщить обо всех проблемах сразу, а не по одной.

Как всё сочетается вместе

В реальном приложении подходы к ошибкам образуют цепочку:

┌─────────────────────────────────────────────────────┐
│                                                     │
│              Клиентский запрос                      │
│                                                     │
└───────────────────┬─────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────┐
│                                                     │
│  Middleware обработка ошибок                        │
│  - Централизует ответы с ошибкам                    │
│  - Логирует ошибки единообразно                     │
│                                                     │
└───────────────────┬─────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────┐
│                                                     │
│  Слой бизнес-логики                                 │
│  - Использует кастомные типы ошибок                 │
│  - Оборачивает ошибки в контекст                    │
│  - Может агрегировать несколько ошибок              │
│                                                     │
└───────────────────┬─────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────┐
│                                                     │
│  Слой доступа к данным                              │
│  - Создаёт специфические типы ошибок                │
│  - Возвращает заранее объявленные ошибки            │
│                                                     │
└─────────────────────────────────────────────────────┘

Частые ошибки новичков

  1. Игнорирование ошибок: не используйте _ = someFunc() , кроме случаев когда у вас на это серьезная причина.

  2. Перекрытие переменной: будьте аккуратны с использованием:= в if . Это может привести к созданию новой локальной переменной**err .**

    var err error
    // ...
    if data, err := json.Marshal(obj); err != nil { // This creates a new 'err'
        return err // Returns the inner err, not the outer one
    }
    // The outer err is unchanged here
    
  3. Возврат nil вместо errors.New(): всегда возвращайте правильное значение ошибки, а не nil.

    // Wrong
    if !valid {
        return nil, nil // Misleading - suggests success but returns no data
    }
    
    // Right
    if !valid {
        return nil, errors.New("validation failed")
    }
    
  4. Избыточное использование panic : оставьте его для действительно фатальных ситуаций.

Заключение

Обработка ошибок в Go — это не только защита от краха программы. Это способ сделать систему устойчивой и понятной. Используя структурированные подходы, добавляя контекст и централизуя управление, можно превратить строгую модель Go в сильное преимущество.

Хорошая обработка ошибок похожа на хорошую документацию: когда всё работает — может казаться лишней, но когда что-то ломается — она бесценна.

Комментарии (1)


  1. starwalkn
    20.08.2025 15:40

    1. Перекрытие переменной: будьте аккуратны с использованием:= в if . Это может привести к созданию новой локальной переменной**err .**

      var err error
      // ...if data, err := json.Marshal(obj); err != nil { // This creates a new 'err'    
      return err // Returns the inner err, not the outer one
      }// The outer err is unchanged here

    Интересно, кому придет в голову вернуть какую-то ошибку из тех что выше, если ошибка произошла при маршалинге?