Обработка ошибок в 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 в сильное преимущество.
Хорошая обработка ошибок похожа на хорошую документацию: когда всё работает — может казаться лишней, но когда что-то ломается — она бесценна.
starwalkn
Интересно, кому придет в голову вернуть какую-то ошибку из тех что выше, если ошибка произошла при маршалинге?