Home » Как добавить дополнительную информацию к ошибкам в Go
Как добавить дополнительную информацию к ошибкам в Go

Как добавить дополнительную информацию к ошибкам в Go

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

Грамотная обработка ошибок — это не просто хорошая практика, это твоя страховка от бессонных ночей и злых писем от заказчиков. Когда твой API падает где-то в глубине микросервиса, а в логах только “database connection failed”, ты понимаешь, что пора было заняться этим вопросом ещё вчера. Сегодня научимся делать ошибки информативными, трассируемыми и полезными.

Как это работает под капотом

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

Начиная с Go 1.13, в стандартной библиотеке появились функции для работы с обёрнутыми ошибками:

// Обёртывание ошибки
err := fmt.Errorf("failed to connect to database: %w", originalErr)

// Проверка типа ошибки
if errors.Is(err, sql.ErrConnDone) {
    // обработка
}

// Извлечение конкретного типа ошибки
var netErr *net.OpError
if errors.As(err, &netErr) {
    // работа с сетевой ошибкой
}

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

Быстрая настройка с pkg/errors

Для начала установим проверенную временем библиотеку pkg/errors:

go get github.com/pkg/errors

Теперь пошагово настроим информативную обработку ошибок:

Шаг 1: Создаём ошибку с контекстом там, где она возникает:

package main

import (
    "database/sql"
    "github.com/pkg/errors"
)

func connectDB(dsn string) (*sql.DB, error) {
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        return nil, errors.Wrap(err, "failed to open database connection")
    }
    
    if err := db.Ping(); err != nil {
        return nil, errors.Wrapf(err, "failed to ping database with DSN: %s", dsn)
    }
    
    return db, nil
}

Шаг 2: Добавляем контекст на каждом уровне:

func initializeApp() error {
    db, err := connectDB(os.Getenv("DATABASE_URL"))
    if err != nil {
        return errors.Wrap(err, "failed to initialize database")
    }
    
    if err := runMigrations(db); err != nil {
        return errors.Wrap(err, "failed to run database migrations")
    }
    
    return nil
}

func main() {
    if err := initializeApp(); err != nil {
        log.Printf("Application failed to start: %+v", err)
        os.Exit(1)
    }
}

Шаг 3: Настраиваем логирование с полным стеком:

// Вместо обычного %v используем %+v для полного стека
log.Printf("Error occurred: %+v", err)

Сравнение подходов к обработке ошибок

Подход Преимущества Недостатки Когда использовать
Стандартный errors Встроен в язык, простой Нет стека вызовов, мало контекста Простые CLI утилиты
pkg/errors Стек вызовов, удобное API Внешняя зависимость, больше не развивается Существующие проекты
fmt.Errorf с %w Стандартная библиотека, цепочка ошибок Нет стека вызовов Новые проекты на Go 1.13+
Кастомные структуры Полный контроль, типизация Много кода, сложность Специфичные требования

Практические примеры и кейсы

Кейс 1: HTTP API с контекстом запроса

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    TraceID string `json:"trace_id"`
    Cause   error  `json:"-"`
}

func (e *APIError) Error() string {
    return fmt.Sprintf("[%s] %s: %v", e.TraceID, e.Message, e.Cause)
}

func handleUser(w http.ResponseWriter, r *http.Request) {
    traceID := r.Header.Get("X-Trace-ID")
    if traceID == "" {
        traceID = generateTraceID()
    }
    
    user, err := getUserFromDB(r.Context(), userID)
    if err != nil {
        apiErr := &APIError{
            Code:    500,
            Message: "Failed to fetch user",
            TraceID: traceID,
            Cause:   errors.Wrap(err, "database query failed"),
        }
        
        log.Printf("API Error: %+v", apiErr)
        writeErrorResponse(w, apiErr)
        return
    }
    
    writeJSONResponse(w, user)
}

Кейс 2: Обработка ошибок в микросервисах

type ServiceError struct {
    Service   string
    Operation string
    UserID    string
    Timestamp time.Time
    Err       error
}

func (e *ServiceError) Error() string {
    return fmt.Sprintf("[%s:%s] user=%s at %s: %v", 
        e.Service, e.Operation, e.UserID, e.Timestamp.Format(time.RFC3339), e.Err)
}

func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) error {
    if err := s.validator.Validate(req); err != nil {
        return &ServiceError{
            Service:   "UserService",
            Operation: "CreateUser",
            UserID:    req.Email,
            Timestamp: time.Now(),
            Err:       errors.Wrap(err, "validation failed"),
        }
    }
    
    if err := s.repo.Save(ctx, req); err != nil {
        return &ServiceError{
            Service:   "UserService",
            Operation: "CreateUser",
            UserID:    req.Email,
            Timestamp: time.Now(),
            Err:       errors.Wrap(err, "failed to save user"),
        }
    }
    
    return nil
}

Продвинутые техники и интеграции

Интеграция с OpenTelemetry для трассировки:

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/codes"
)

func processOrder(ctx context.Context, orderID string) error {
    tracer := otel.Tracer("order-service")
    ctx, span := tracer.Start(ctx, "process-order")
    defer span.End()
    
    if err := validateOrder(ctx, orderID); err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, "order validation failed")
        return errors.Wrapf(err, "failed to validate order %s", orderID)
    }
    
    return nil
}

Интеграция с Sentry для мониторинга:

import "github.com/getsentry/sentry-go"

func handleError(err error, ctx context.Context) {
    if err != nil {
        sentry.WithScope(func(scope *sentry.Scope) {
            scope.SetContext("error_details", map[string]interface{}{
                "stack_trace": fmt.Sprintf("%+v", err),
                "user_id":     getUserID(ctx),
                "request_id":  getRequestID(ctx),
            })
            sentry.CaptureException(err)
        })
    }
}

Автоматизация и скрипты

Для автоматизации анализа ошибок можно создать утилиту для парсинга логов:

#!/bin/bash
# error_analyzer.sh

# Анализ ошибок в логах Go приложения
log_file="/var/log/myapp/app.log"

echo "=== Top 10 errors by frequency ==="
grep -E "Error|ERROR|panic" "$log_file" | \
  sed 's/.*ERROR \[.*\] //' | \
  sort | uniq -c | sort -nr | head -10

echo "=== Errors by service ==="
grep -E "Error|ERROR" "$log_file" | \
  grep -oE '\[.*Service.*\]' | \
  sort | uniq -c | sort -nr

echo "=== Database connection errors ==="
grep -c "database connection failed" "$log_file"

echo "=== Memory/Performance related errors ==="
grep -E "out of memory|timeout|context deadline exceeded" "$log_file" | wc -l

Для продакшн-серверов рекомендую настроить VPS с достаточным объёмом RAM для логирования, а для высоконагруженных проектов — выделенный сервер.

Альтернативные решения

Помимо pkg/errors, стоит рассмотреть:

  • go-errors/errors — альтернатива с поддержкой стека вызовов
  • hashicorp/go-multierror — для агрегации множественных ошибок
  • uber-go/zap — структурированное логирование с поддержкой ошибок
  • sirupsen/logrus — популярный логгер с хуками для ошибок

Для мониторинга рекомендую:

  • Prometheus + Grafana — метрики ошибок
  • ELK Stack — централизованное логирование
  • Jaeger — распределённая трассировка

Интересные факты и нестандартные применения

Мало кто знает, что можно использовать ошибки для создания цепочки выполнения команд:

type CommandError struct {
    Command string
    Args    []string
    Output  string
    Err     error
}

func (c *CommandError) Error() string {
    return fmt.Sprintf("command '%s %v' failed: %v\nOutput: %s", 
        c.Command, c.Args, c.Err, c.Output)
}

// Использование для автоматизации деплоя
func deployApp() error {
    commands := [][]string{
        {"git", "pull", "origin", "main"},
        {"go", "build", "-o", "app"},
        {"systemctl", "restart", "myapp"},
    }
    
    for _, cmd := range commands {
        if err := executeCommand(cmd); err != nil {
            return &CommandError{
                Command: cmd[0],
                Args:    cmd[1:],
                Output:  getCommandOutput(cmd),
                Err:     err,
            }
        }
    }
    
    return nil
}

Также можно создать middleware для автоматического логирования всех ошибок:

func ErrorLoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                err, ok := r.(error)
                if !ok {
                    err = fmt.Errorf("panic: %v", r)
                }
                
                log.Printf("Panic recovered: %+v", errors.Wrap(err, "request panic"))
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        
        next.ServeHTTP(w, r)
    })
}

Статистика и производительность

Согласно бенчмаркам, использование pkg/errors добавляет около 20-30% накладных расходов по сравнению со стандартными ошибками, но экономит часы на дебаге. В высоконагруженных системах (>10000 RPS) рекомендую использовать ошибки с контекстом только для критических путей.

Сравнение производительности:

BenchmarkStandardError-8    100000000    10.5 ns/op    0 B/op    0 allocs/op
BenchmarkPkgErrors-8        50000000     35.2 ns/op    64 B/op   2 allocs/op
BenchmarkCustomError-8      75000000     18.7 ns/op    32 B/op   1 allocs/op

Новые возможности в автоматизации

С продвинутой обработкой ошибок открываются новые возможности:

  • Автоматическое восстановление — анализ типа ошибки и попытка исправления
  • Умная балансировка нагрузки — исключение неисправных инстансов по типу ошибок
  • Предиктивное масштабирование — анализ паттернов ошибок для предсказания нагрузки
  • Автоматические алерты — настройка уведомлений по критическим ошибкам

Пример автоматического восстановления:

func autoRecovery(err error) bool {
    switch {
    case errors.Is(err, sql.ErrConnDone):
        return reconnectDB()
    case errors.Is(err, context.DeadlineExceeded):
        return increaseTimeout()
    case isNetworkError(err):
        return switchToBackupEndpoint()
    default:
        return false
    }
}

Заключение и рекомендации

Грамотная обработка ошибок — это инвестиция в будущее твоего проекта. Начни с малого: добавь контекст к критическим местам, настрой структурированное логирование, подключи мониторинг. Для новых проектов рекомендую использовать стандартные средства Go 1.13+ с fmt.Errorf и %w, для существующих — pkg/errors остаётся отличным выбором.

Помни основные принципы:

  • Добавляй контекст на каждом уровне
  • Используй типизированные ошибки для важной бизнес-логики
  • Не игнорируй ошибки — обрабатывай или прокидывай выше
  • Логируй с полным стеком в продакшене
  • Настрой мониторинг и алертинг

Качественная обработка ошибок превратит твой код из “работает на моей машине” в надёжный продакшн-сервис. А твоя будущая версия скажет спасибо за сэкономленные нервы и время.


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

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

Leave a reply

Your email address will not be published. Required fields are marked