Обработка ошибок в 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, ¬FoundErr) { 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 обработка ошибок │ │ - Централизует ответы с ошибкам │ │ - Логирует ошибки единообразно │ │ │ └───────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ │ │ Слой бизнес-логики │ │ - Использует кастомные типы ошибок │ │ - Оборачивает ошибки в контекст │ │ - Может агрегировать несколько ошибок │ │ │ └───────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ │ │ Слой доступа к данным │ │ - Создаёт специфические типы ошибок │ │ - Возвращает заранее объявленные ошибки │ │ │ └─────────────────────────────────────────────────────┘
Частые ошибки новичков
-
Игнорирование ошибок: не используйте
_ = someFunc(), кроме случаев когда у вас на это серьезная причина. -
Перекрытие переменной: будьте аккуратны с использованием
:=в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 -
Возврат
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") } -
Избыточное использование
panic: оставьте его для действительно фатальных ситуаций.
Заключение
Обработка ошибок в Go — это не только защита от краха программы. Это способ сделать систему устойчивой и понятной. Используя структурированные подходы, добавляя контекст и централизуя управление, можно превратить строгую модель Go в сильное преимущество.
Хорошая обработка ошибок похожа на хорошую документацию: когда всё работает — может казаться лишней, но когда что-то ломается — она бесценна.
ссылка на оригинал статьи https://habr.com/ru/articles/938928/
Добавить комментарий