Паттерны обработки ошибок в GO: это должен знать каждый разработчик

от автора

Обработка ошибок в 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 в сильное преимущество.

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


ссылка на оригинал статьи https://habr.com/ru/articles/938928/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *