Skip to content

自定义错误

1. 概述

在 Go 语言中,错误是一个实现了 error 接口的类型。虽然标准库提供了基本的错误创建方法,但在实际开发中,我们经常需要创建带有额外信息的自定义错误,以便更好地描述错误的性质和上下文。

本章节将详细介绍如何创建和使用自定义错误,包括自定义错误的实现方式、最佳实践以及在实际项目中的应用场景。

2. 基本概念

2.1 语法

创建自定义错误的基本语法:

go
// 定义自定义错误类型
type MyError struct {
    // 自定义字段
    Message string
    Code    int
    Cause   error
}

// 实现 error 接口
func (e *MyError) Error() string {
    return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}

// 可选:实现 Unwrap 方法,用于错误链
func (e *MyError) Unwrap() error {
    return e.Cause
}

// 创建自定义错误的函数
func NewMyError(code int, message string, cause error) *MyError {
    return &MyError{Code: code, Message: message, Cause: cause}
}

2.2 语义

  • 自定义错误类型:通过实现 error 接口创建的错误类型
  • 错误字段:自定义错误中包含的额外信息,如错误码、错误消息、原始错误等
  • 错误链:通过实现 Unwrap() 方法,将自定义错误与原始错误链接起来

2.3 规范

  • 自定义错误类型应该提供清晰、准确的错误信息
  • 自定义错误类型应该包含足够的上下文信息
  • 包装其他错误的自定义错误应该实现 Unwrap() 方法
  • 错误类型的命名应该清晰反映其用途

3. 原理深度解析

3.1 自定义错误的实现原理

自定义错误的核心是实现 error 接口,该接口定义如下:

go
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都可以作为错误使用。为了支持错误链,自定义错误还可以实现 Unwrap() error 方法,该方法返回被包装的原始错误。

3.2 错误链的实现

错误链是通过 Unwrap() 方法实现的,当使用 errors.Is()errors.As() 函数时,这些函数会遍历错误链,找到原始错误或特定类型的错误。

go
// 错误链的遍历逻辑
type causer interface {
    Cause() error
}

type unwrapper interface {
    Unwrap() error
}

func is(err, target error) bool {
    if err == target {
        return true
    }
    
    // 尝试通过 Unwrap() 方法获取原始错误
    if u, ok := err.(unwrapper); ok {
        if is(u.Unwrap(), target) {
            return true
        }
    }
    
    // 尝试通过 Cause() 方法获取原始错误(兼容第三方库)
    if c, ok := err.(causer); ok {
        if is(c.Cause(), target) {
            return true
        }
    }
    
    return false
}

4. 常见错误与踩坑点

4.1 忘记实现 Error() 方法

错误表现:自定义类型无法作为错误使用

产生原因:没有实现 error 接口的 Error() string 方法

解决方案:确保自定义错误类型实现了 Error() string 方法

4.2 没有实现 Unwrap() 方法

错误表现:包装错误后,无法通过 errors.Is()errors.As() 函数正确处理错误链

产生原因:自定义错误类型包装了其他错误,但没有实现 Unwrap() 方法

解决方案:为包装了其他错误的自定义错误类型实现 Unwrap() 方法

4.3 错误信息不够详细

错误表现:错误信息过于简单,无法提供足够的上下文信息

产生原因:自定义错误类型没有包含足够的上下文信息

解决方案:在自定义错误类型中添加必要的字段,提供详细的错误信息

4.4 错误类型判断错误

错误表现:使用 == 直接比较自定义错误,导致判断失败

产生原因:不同的错误实例即使字段值相同,也不是同一个对象

解决方案:使用 errors.As() 函数进行错误类型判断

5. 常见应用场景

5.1 带错误码的错误

场景描述:需要在错误中包含错误码,以便客户端根据错误码采取不同的处理策略

使用方法:定义包含错误码字段的自定义错误类型

示例代码

go
type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

func NewAppError(code int, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

// 使用示例
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, NewAppError(400, "division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        if appErr, ok := err.(*AppError); ok {
            fmt.Printf("Error code: %d\n", appErr.Code)
        }
        return
    }
    fmt.Printf("Result: %d\n", result)
}

5.2 带上下文信息的错误

场景描述:需要在错误中包含更多的上下文信息,如请求 ID、用户 ID 等

使用方法:定义包含上下文信息字段的自定义错误类型

示例代码

go
type ContextError struct {
    Message   string
    RequestID string
    UserID    string
    Cause     error
}

func (e *ContextError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("%s (request ID: %s, user ID: %s): %v", e.Message, e.RequestID, e.UserID, e.Cause)
    }
    return fmt.Sprintf("%s (request ID: %s, user ID: %s)", e.Message, e.RequestID, e.UserID)
}

func (e *ContextError) Unwrap() error {
    return e.Cause
}

func NewContextError(message, requestID, userID string, cause error) *ContextError {
    return &ContextError{Message: message, RequestID: requestID, UserID: userID, Cause: cause}
}

// 使用示例
func processRequest(req Request) error {
    // 验证请求
    if err := validateRequest(req); err != nil {
        return NewContextError("invalid request", req.ID, req.UserID, err)
    }
    
    // 处理请求
    if err := saveRequest(req); err != nil {
        return NewContextError("failed to save request", req.ID, req.UserID, err)
    }
    
    return nil
}

5.3 业务逻辑错误

场景描述:需要定义特定业务逻辑的错误类型,如验证错误、权限错误等

使用方法:为不同类型的业务错误定义不同的自定义错误类型

示例代码

go
// 验证错误
type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on field %s with value %v: %s", e.Field, e.Value, e.Message)
}

func NewValidationError(field string, value interface{}, message string) *ValidationError {
    return &ValidationError{Field: field, Value: value, Message: message}
}

// 权限错误
type PermissionError struct {
    UserID   string
    Resource string
    Action   string
}

func (e *PermissionError) Error() string {
    return fmt.Sprintf("user %s does not have permission to %s %s", e.UserID, e.Action, e.Resource)
}

func NewPermissionError(userID, resource, action string) *PermissionError {
    return &PermissionError{UserID: userID, Resource: resource, Action: action}
}

// 使用示例
func validateUser(user User) error {
    if user.Name == "" {
        return NewValidationError("name", user.Name, "name is required")
    }
    if user.Age < 18 {
        return NewValidationError("age", user.Age, "age must be at least 18")
    }
    return nil
}

func checkPermission(userID, resource, action string) error {
    // 检查权限
    if !hasPermission(userID, resource, action) {
        return NewPermissionError(userID, resource, action)
    }
    return nil
}

5.4 带堆栈信息的错误

场景描述:需要在错误中包含堆栈信息,以便更好地定位错误发生的位置

使用方法:定义包含堆栈信息字段的自定义错误类型

示例代码

go
import (
    "bytes"
    "debug"
    "fmt"
)

type StackError struct {
    Message    string
    StackTrace string
    Cause      error
}

func (e *StackError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("%s\nStack trace:\n%s\nCause: %v", e.Message, e.StackTrace, e.Cause)
    }
    return fmt.Sprintf("%s\nStack trace:\n%s", e.Message, e.StackTrace)
}

func (e *StackError) Unwrap() error {
    return e.Cause
}

func captureStackTrace() string {
    var buf bytes.Buffer
    stackTrace := debug.Stack()
    buf.Write(stackTrace)
    return buf.String()
}

func NewStackError(message string, cause error) *StackError {
    return &StackError{
        Message:    message,
        StackTrace: captureStackTrace(),
        Cause:      cause,
    }
}

// 使用示例
func processFile(filename string) error {
    data, err := os.ReadFile(filename)
    if err != nil {
        return NewStackError("failed to read file", err)
    }
    // 处理数据
    return nil
}

5.5 可恢复的错误

场景描述:需要标识某些错误是可恢复的,可以通过重试等方式解决

使用方法:定义包含可恢复标识的自定义错误类型

示例代码

go
type RecoverableError struct {
    Message   string
    Cause     error
    Retryable bool
}

func (e *RecoverableError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("%s: %v", e.Message, e.Cause)
    }
    return e.Message
}

func (e *RecoverableError) Unwrap() error {
    return e.Cause
}

func NewRecoverableError(message string, cause error, retryable bool) *RecoverableError {
    return &RecoverableError{Message: message, Cause: cause, Retryable: retryable}
}

// 使用示例
func fetchData(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return NewRecoverableError("failed to fetch data", err, true)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
    }
    
    // 处理响应
    return nil
}

// 尝试执行操作,处理可恢复的错误
func TryOperation(attempts int, f func() error) error {
    for i := 0; i < attempts; i++ {
        err := f()
        if err == nil {
            return nil
        }
        
        var recoverableErr *RecoverableError
        if errors.As(err, &recoverableErr) && recoverableErr.Retryable {
            log.Printf("Attempt %d failed, retrying: %v\n", i+1, err)
            time.Sleep(time.Second * time.Duration(i+1))
            continue
        }
        
        return err
    }
    return fmt.Errorf("failed after %d attempts", attempts)
}

6. 企业级进阶应用场景

6.1 错误分类系统

场景描述:在企业级应用中,需要对错误进行分类,以便采取不同的处理策略

使用方法:创建带有错误类型的自定义错误,并实现错误分类系统

示例代码

go
// 错误类型定义
const (
    ErrorTypeValidation = iota
    ErrorTypeDatabase
    ErrorTypeNetwork
    ErrorTypeInternal
)

// 自定义错误类型
type AppError struct {
    Type    int
    Code    int
    Message string
    Cause   error
}

func (e *AppError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("%s (code: %d): %v", e.Message, e.Code, e.Cause)
    }
    return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}

func (e *AppError) Unwrap() error {
    return e.Cause
}

// 创建不同类型的错误
func NewValidationError(code int, message string, cause error) *AppError {
    return &AppError{Type: ErrorTypeValidation, Code: code, Message: message, Cause: cause}
}

func NewDatabaseError(code int, message string, cause error) *AppError {
    return &AppError{Type: ErrorTypeDatabase, Code: code, Message: message, Cause: cause}
}

func NewNetworkError(code int, message string, cause error) *AppError {
    return &AppError{Type: ErrorTypeNetwork, Code: code, Message: message, Cause: cause}
}

func NewInternalError(code int, message string, cause error) *AppError {
    return &AppError{Type: ErrorTypeInternal, Code: code, Message: message, Cause: cause}
}

// 错误处理策略
var errorHandlers = map[int]func(*AppError) error{
    ErrorTypeValidation: handleValidationError,
    ErrorTypeDatabase:   handleDatabaseError,
    ErrorTypeNetwork:    handleNetworkError,
    ErrorTypeInternal:   handleInternalError,
}

func handleValidationError(err *AppError) error {
    // 处理验证错误
    log.Printf("Validation error: %v\n", err)
    return nil
}

func handleDatabaseError(err *AppError) error {
    // 处理数据库错误
    log.Printf("Database error: %v\n", err)
    // 可以尝试重试
    return retryDatabaseOperation(err)
}

func handleNetworkError(err *AppError) error {
    // 处理网络错误
    log.Printf("Network error: %v\n", err)
    // 可以尝试重试
    return retryNetworkOperation(err)
}

func handleInternalError(err *AppError) error {
    // 处理内部错误
    log.Printf("Internal error: %v\n", err)
    // 发送报警
    sendAlert(err)
    return err
}

// 统一错误处理函数
func HandleError(err error) error {
    var appErr *AppError
    if errors.As(err, &appErr) {
        if handler, ok := errorHandlers[appErr.Type]; ok {
            return handler(appErr)
        }
    }
    // 处理未知错误
    log.Printf("Unknown error: %v\n", err)
    return err
}

6.2 错误监控系统

场景描述:在企业级应用中,需要监控错误的发生情况,统计错误类型和频率

使用方法:创建带有监控信息的自定义错误,并实现错误监控系统

示例代码

go
// 错误监控信息
type MonitoredError struct {
    err        error
    timestamp  time.Time
    service    string
    environment string
    stackTrace string
}

func (e *MonitoredError) Error() string {
    return e.err.Error()
}

func (e *MonitoredError) Unwrap() error {
    return e.err
}

func MonitorError(err error, service, environment string) error {
    if err == nil {
        return nil
    }
    
    // 捕获堆栈信息
    stackTrace := captureStackTrace()
    
    // 创建监控错误
    monitoredErr := &MonitoredError{
        err:         err,
        timestamp:   time.Now(),
        service:     service,
        environment: environment,
        stackTrace:  stackTrace,
    }
    
    // 记录错误信息
    log.Printf("Error occurred in %s (%s): %v\nStack trace: %s\n", service, environment, err, stackTrace)
    
    // 发送到监控系统
    sendToMonitoringSystem(monitoredErr)
    
    return monitoredErr
}

// 使用示例
func processRequest(req Request) error {
    err := validateRequest(req)
    if err != nil {
        return MonitorError(fmt.Errorf("invalid request: %w", err), "api-service", "production")
    }
    
    err = saveRequest(req)
    if err != nil {
        return MonitorError(fmt.Errorf("failed to save request: %w", err), "api-service", "production")
    }
    
    return nil
}

6.3 错误国际化

场景描述:在企业级应用中,需要支持多语言错误信息

使用方法:创建带有国际化支持的自定义错误类型

示例代码

go
// 国际化错误
type I18nError struct {
    Key     string
    Params  map[string]interface{}
    Locale  string
    Cause   error
}

func (e *I18nError) Error() string {
    message := translate(e.Key, e.Params, e.Locale)
    if e.Cause != nil {
        return fmt.Sprintf("%s: %v", message, e.Cause)
    }
    return message
}

func (e *I18nError) Unwrap() error {
    return e.Cause
}

func NewI18nError(key string, params map[string]interface{}, locale string, cause error) *I18nError {
    return &I18nError{Key: key, Params: params, Locale: locale, Cause: cause}
}

// 翻译函数(示例实现)
func translate(key string, params map[string]interface{}, locale string) string {
    // 从翻译文件中获取翻译
    // 这里只是示例,实际实现可能会从数据库或配置文件中获取
    translations := map[string]map[string]string{
        "en": {
            "error.division_by_zero": "Division by zero",
            "error.invalid_input": "Invalid input: {{field}}",
        },
        "zh": {
            "error.division_by_zero": "除零错误",
            "error.invalid_input": "无效输入:{{field}}",
        },
    }
    
    if langTranslations, ok := translations[locale]; ok {
        if translation, ok := langTranslations[key]; ok {
            // 替换参数
            result := translation
            for k, v := range params {
                placeholder := fmt.Sprintf("{{%s}}", k)
                result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", v))
            }
            return result
        }
    }
    
    // 如果没有找到翻译,返回键
    return key
}

// 使用示例
func divide(a, b int, locale string) (int, error) {
    if b == 0 {
        return 0, NewI18nError("error.division_by_zero", nil, locale, nil)
    }
    return a / b, nil
}

func validateUser(user User, locale string) error {
    if user.Name == "" {
        params := map[string]interface{}{"field": "name"}
        return NewI18nError("error.invalid_input", params, locale, nil)
    }
    return nil
}

7. 行业最佳实践

7.1 为自定义错误实现 Unwrap 方法

实践内容:为包装了其他错误的自定义错误类型实现 Unwrap() 方法

推荐理由:这样可以确保 errors.Is()errors.As() 函数能够正确处理错误链

7.2 错误信息应该包含上下文

实践内容:在自定义错误中包含足够的上下文信息

推荐理由:上下文信息有助于快速定位问题,减少调试时间

7.3 为不同类型的错误定义不同的错误类型

实践内容:为不同类型的错误(如验证错误、权限错误等)定义不同的自定义错误类型

推荐理由:这样可以根据错误类型采取不同的处理策略,提高错误处理的灵活性

7.4 统一错误创建方式

实践内容:创建统一的错误创建函数,确保错误格式一致

推荐理由:统一的错误格式有助于错误处理的一致性,便于错误监控和统计

7.5 错误类型的命名应该清晰

实践内容:为自定义错误类型选择清晰、描述性的名称

推荐理由:清晰的命名有助于理解错误的性质和用途

8. 常见问题答疑(FAQ)

8.1 问:如何判断一个错误是否是自定义错误类型?

回答:使用 errors.As() 函数进行错误类型判断

示例代码

go
var appErr *AppError
if errors.As(err, &appErr) {
    // 处理 AppError 类型的错误
    fmt.Printf("Error code: %d\n", appErr.Code)
}

8.2 问:如何在自定义错误中包含堆栈信息?

回答:使用 debug 包捕获堆栈信息,并将其包含在自定义错误中

示例代码

go
func captureStackTrace() string {
    var buf bytes.Buffer
    stackTrace := debug.Stack()
    buf.Write(stackTrace)
    return buf.String()
}

type StackError struct {
    Message    string
    StackTrace string
    Cause      error
}

func (e *StackError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("%s\nStack trace:\n%s\nCause: %v", e.Message, e.StackTrace, e.Cause)
    }
    return fmt.Sprintf("%s\nStack trace:\n%s", e.Message, e.StackTrace)
}

func (e *StackError) Unwrap() error {
    return e.Cause
}

func NewStackError(message string, cause error) *StackError {
    return &StackError{
        Message:    message,
        StackTrace: captureStackTrace(),
        Cause:      cause,
    }
}

8.3 问:如何创建带有错误码的自定义错误?

回答:定义包含错误码字段的自定义错误类型

示例代码

go
type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

func NewAppError(code int, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

8.4 问:如何处理多层嵌套的自定义错误?

回答:实现 Unwrap() 方法,使用 errors.Is()errors.As() 函数遍历错误链

示例代码

go
type AppError struct {
    Code    int
    Message string
    Cause   error
}

func (e *AppError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("%s (code: %d): %v", e.Message, e.Code, e.Cause)
    }
    return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}

func (e *AppError) Unwrap() error {
    return e.Cause
}

// 使用示例
func level1() error {
    if err := level2(); err != nil {
        return &AppError{Code: 500, Message: "level1 error", Cause: err}
    }
    return nil
}

func level2() error {
    if err := level3(); err != nil {
        return &AppError{Code: 500, Message: "level2 error", Cause: err}
    }
    return nil
}

func level3() error {
    return os.ErrNotExist
}

func main() {
    err := level1()
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("Original error is os.ErrNotExist")
        }
    }
}

8.5 问:自定义错误会影响性能吗?

回答:自定义错误的开销很小,主要是创建一个新的结构体实例。在大多数情况下,这种开销可以忽略不计

示例代码

go
// 自定义错误的性能测试
func benchmarkCustomError(b *testing.B) {
    for i := 0; i < b.N; i++ {
        err := NewAppError(400, "test error")
        _ = err
    }
}

8.6 问:如何在 HTTP 响应中返回自定义错误?

回答:将自定义错误转换为适当的 HTTP 响应

示例代码

go
func handleRequest(w http.ResponseWriter, r *http.Request) {
    // 处理请求
    err := processRequest(r)
    if err != nil {
        var appErr *AppError
        if errors.As(err, &appErr) {
            // 根据错误码设置 HTTP 状态码
            statusCode := http.StatusInternalServerError
            switch appErr.Code {
            case 400:
                statusCode = http.StatusBadRequest
            case 401:
                statusCode = http.StatusUnauthorized
            case 403:
                statusCode = http.StatusForbidden
            case 404:
                statusCode = http.StatusNotFound
            }
            
            // 返回 JSON 响应
            response := map[string]interface{}{
                "error": map[string]interface{}{
                    "code":    appErr.Code,
                    "message": appErr.Message,
                },
            }
            
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(statusCode)
            json.NewEncoder(w).Encode(response)
            return
        }
        
        // 处理其他错误
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
    
    // 返回成功响应
    w.WriteHeader(http.StatusOK)
    fmt.Fprintln(w, "OK")
}

9. 实战练习

9.1 基础练习

练习内容:实现一个带有错误码和错误信息的自定义错误类型

解题思路

  1. 定义一个结构体,包含错误码和错误信息字段
  2. 实现 Error() string 方法
  3. 创建函数返回自定义错误
  4. 测试错误处理

常见误区:忘记实现 Error() string 方法,或者错误信息不清晰

分步提示

  1. 定义结构体 AppError,包含 CodeMessage 字段
  2. 实现 Error() string 方法,返回格式化的错误信息
  3. 创建函数 NewAppError,用于创建新的 AppError
  4. 编写测试代码,验证错误处理

参考代码

go
package main

import (
    "fmt"
)

type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

func NewAppError(code int, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, NewAppError(400, "division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        if appErr, ok := err.(*AppError); ok {
            fmt.Printf("Error code: %d\n", appErr.Code)
        }
        return
    }
    fmt.Printf("Result: %d\n", result)
}

9.2 进阶练习

练习内容:实现一个带有上下文信息和错误链的自定义错误类型

解题思路

  1. 定义一个结构体,包含错误信息、上下文信息和原始错误字段
  2. 实现 Error() string 方法,返回格式化的错误信息
  3. 实现 Unwrap() error 方法,返回原始错误
  4. 创建函数返回自定义错误
  5. 测试错误链的处理

常见误区:没有实现 Unwrap() error 方法,导致错误链断裂

分步提示

  1. 定义结构体 ContextError,包含 MessageContextCause 字段
  2. 实现 Error() string 方法,返回格式化的错误信息
  3. 实现 Unwrap() error 方法,返回原始错误
  4. 创建函数 NewContextError,用于创建新的 ContextError
  5. 编写测试代码,验证错误链的处理

参考代码

go
package main

import (
    "errors"
    "fmt"
    "os"
)

type ContextError struct {
    Message string
    Context map[string]interface{}
    Cause   error
}

func (e *ContextError) Error() string {
    contextStr := ""
    if len(e.Context) > 0 {
        contextStr = " (context: "
        for k, v := range e.Context {
            contextStr += fmt.Sprintf("%s=%v, ", k, v)
        }
        contextStr = contextStr[:len(contextStr)-2] + ")"
    }
    
    if e.Cause != nil {
        return fmt.Sprintf("%s%s: %v", e.Message, contextStr, e.Cause)
    }
    return fmt.Sprintf("%s%s", e.Message, contextStr)
}

func (e *ContextError) Unwrap() error {
    return e.Cause
}

func NewContextError(message string, context map[string]interface{}, cause error) *ContextError {
    if context == nil {
        context = make(map[string]interface{})
    }
    return &ContextError{Message: message, Context: context, Cause: cause}
}

func level1() error {
    context := map[string]interface{}{"level": 1, "operation": "level1"}
    if err := level2(); err != nil {
        return NewContextError("level1 error", context, err)
    }
    return nil
}

func level2() error {
    context := map[string]interface{}{"level": 2, "operation": "level2"}
    if err := level3(); err != nil {
        return NewContextError("level2 error", context, err)
    }
    return nil
}

func level3() error {
    return os.ErrNotExist
}

func main() {
    err := level1()
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("Original error is os.ErrNotExist")
        }
    }
}

9.3 挑战练习

练习内容:实现一个错误处理中间件,处理自定义错误并返回适当的 HTTP 响应

解题思路

  1. 定义一个 HTTP 中间件函数,接受下一个处理函数
  2. 在中间件中执行下一个处理函数,捕获错误
  3. 根据错误类型返回适当的 HTTP 响应
  4. 测试中间件功能

常见误区:没有正确处理不同类型的错误,或者没有返回适当的 HTTP 响应

分步提示

  1. 定义中间件函数 ErrorHandlerMiddleware,签名为 func(http.Handler) http.Handler
  2. 实现中间件逻辑,包括错误捕获和处理
  3. 创建一个 responseRecorder 来记录响应状态码
  4. 根据错误类型返回适当的 HTTP 响应
  5. 编写测试代码,验证中间件功能

参考代码

go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/http/httptest"
)

type AppError struct {
    Code    int
    Message string
}

func (e *AppError) Error() string {
    return fmt.Sprintf("Error %d: %s", e.Code, e.Message)
}

func NewAppError(code int, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

// 错误处理中间件
func ErrorHandlerMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // 记录 panic
                panicErr := NewAppError(http.StatusInternalServerError, "Internal Server Error")
                log.Printf("Panic recovered: %v\n", err)
                sendErrorResponse(w, panicErr)
            }
        }()
        
        // 执行下一个处理函数
        next.ServeHTTP(w, r)
    })
}

// 发送错误响应
func sendErrorResponse(w http.ResponseWriter, err error) {
    var appErr *AppError
    if ae, ok := err.(*AppError); ok {
        appErr = ae
    } else {
        appErr = NewAppError(http.StatusInternalServerError, "Internal Server Error")
    }
    
    // 设置 HTTP 状态码
    statusCode := http.StatusInternalServerError
    switch appErr.Code {
    case 400:
        statusCode = http.StatusBadRequest
    case 401:
        statusCode = http.StatusUnauthorized
    case 403:
        statusCode = http.StatusForbidden
    case 404:
        statusCode = http.StatusNotFound
    }
    
    // 返回 JSON 响应
    response := map[string]interface{}{
        "error": map[string]interface{}{
            "code":    appErr.Code,
            "message": appErr.Message,
        },
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)
    json.NewEncoder(w).Encode(response)
}

// 测试处理函数
func testHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/error" {
        err := NewAppError(400, "Bad Request")
        sendErrorResponse(w, err)
        return
    }
    fmt.Fprintln(w, "Hello, World!")
}

func main() {
    // 创建测试服务器
    handler := ErrorHandlerMiddleware(http.HandlerFunc(testHandler))
    
    // 测试正常请求
    req, _ := http.NewRequest("GET", "/", nil)
    rr := httptest.NewRecorder()
    handler.ServeHTTP(rr, req)
    fmt.Printf("Normal request: %d %s\n", rr.Code, rr.Body.String())
    
    // 测试错误请求
    req, _ = http.NewRequest("GET", "/error", nil)
    rr = httptest.NewRecorder()
    handler.ServeHTTP(rr, req)
    fmt.Printf("Error request: %d %s\n", rr.Code, rr.Body.String())
    
    // 启动服务器
    http.ListenAndServe(":8080", handler)
}

10. 知识点总结

10.1 核心要点

  • 自定义错误是通过实现 error 接口创建的
  • 自定义错误可以包含额外的字段,如错误码、上下文信息等
  • 包装其他错误的自定义错误应该实现 Unwrap() 方法,以便支持错误链
  • 使用 errors.As() 函数可以判断错误是否是特定类型的自定义错误
  • 自定义错误可以根据业务需求进行扩展,如添加堆栈信息、国际化支持等

10.2 易错点回顾

  • 忘记实现 Error() string 方法,导致自定义类型无法作为错误使用
  • 没有实现 Unwrap() 方法,导致错误链断裂
  • 错误信息不够详细,无法提供足够的上下文信息
  • 使用 == 直接比较自定义错误,导致判断失败
  • 没有为不同类型的错误定义不同的错误类型,导致错误处理不够灵活

11. 拓展参考资料

11.1 官方文档链接

11.2 进阶学习路径建议

  • 学习 context 包,了解如何在错误处理中使用上下文
  • 学习 errgroup 包,掌握并发错误处理
  • 学习第三方错误处理库,如 pkg/errors
  • 学习如何在大型项目中设计错误处理策略

12. 代码规范

12.1 自定义错误代码风格

  • 自定义错误类型的命名应该清晰、描述性
  • 错误信息应该清晰、具体,包含足够的上下文
  • 包装其他错误的自定义错误应该实现 Unwrap() 方法
  • 为自定义错误提供创建函数,确保错误格式一致
  • 错误码应该有明确的含义和范围

12.2 示例代码

go
// 定义错误变量
var (
    ErrNotFound      = errors.New("resource not found")
    ErrInvalidInput  = errors.New("invalid input")
    ErrInternalError = errors.New("internal error")
)

// 自定义错误类型
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}

// 创建自定义错误的函数
func NewValidationError(field, message string) *ValidationError {
    return &ValidationError{Field: field, Message: message}
}

// 带错误链的自定义错误
type AppError struct {
    Code    int
    Message string
    Cause   error
}

func (e *AppError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("%s (code: %d): %v", e.Message, e.Code, e.Cause)
    }
    return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}

func (e *AppError) Unwrap() error {
    return e.Cause
}

func NewAppError(code int, message string, cause error) *AppError {
    return &AppError{Code: code, Message: message, Cause: cause}
}

// 使用示例
func validateUser(user User) error {
    if user.Name == "" {
        return NewValidationError("name", "name is required")
    }
    return nil
}

func getUser(id int) (User, error) {
    user, err := db.GetUser(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return User{}, NewAppError(404, "user not found", err)
        }
        return User{}, NewAppError(500, "failed to get user", err)
    }
    return user, nil
}

本章节介绍了 Go 语言中自定义错误的实现和使用方法。通过学习这些内容,开发者可以创建更加灵活、功能丰富的错误类型,提高代码的可读性和可维护性。