Announcement

👇Official Account👇

图片

Welcome to join the group & private message

Article first/tail QR code

Skip to content

Go 错误处理最佳实践:从 error 到 wrapping

Go 的错误处理是语言设计中最具争议性的部分,也是最能体现 Go 哲学的地方。本文从基础到生产实践,系统梳理 Go 错误处理的正确姿势。

一、Go 错误处理的哲学

Go 通过多返回值显式返回错误,而不是异常机制:

go
f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close()

这种设计让错误处理可见、可控,但也要求开发者认真对待每一个错误。

二、error 接口

error 是一个简单接口:

go
type error interface {
    Error() string
}

标准库的 errors.Newfmt.Errorf 是最常用的创建方式:

go
import "errors"

var ErrNotFound = errors.New("record not found")

func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid id: %d", id)
    }
    // ...
}

三、Sentinel Error(哨兵错误)

Sentinel error 是预定义的、可被比较的错误变量,适用于表达特定的错误条件:

go
package main

import (
    "errors"
    "fmt"
)

// 定义哨兵错误(包级变量)
var (
    ErrNotFound   = errors.New("not found")
    ErrPermission = errors.New("permission denied")
    ErrTimeout    = errors.New("operation timeout")
)

func getResource(id int) (string, error) {
    if id == 0 {
        return "", ErrNotFound
    }
    if id < 0 {
        return "", ErrPermission
    }
    return "resource", nil
}

func main() {
    _, err := getResource(0)
    
    // 使用 errors.Is 比较(推荐,支持 wrapping)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("资源不存在,执行创建逻辑")
    }
}

命名规范:Sentinel error 以 Err 前缀命名,如 ErrNotFoundErrInvalidInput

四、自定义错误类型

当需要携带额外上下文信息时,自定义错误类型更合适:

go
// 自定义错误结构体
type ValidationError struct {
    Field   string
    Message string
    Code    int
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field '%s': %s (code: %d)",
        e.Field, e.Message, e.Code)
}

// 使用 errors.As 提取具体类型
func processInput(name string) error {
    if len(name) == 0 {
        return &ValidationError{
            Field:   "name",
            Message: "不能为空",
            Code:    400,
        }
    }
    if len(name) > 50 {
        return &ValidationError{
            Field:   "name",
            Message: "长度不能超过 50",
            Code:    400,
        }
    }
    return nil
}

func main() {
    err := processInput("")
    
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("字段: %s, 错误码: %d\n", valErr.Field, valErr.Code)
        // 可以访问具体字段,做针对性处理
    }
}

五、Error Wrapping(错误包装)

Go 1.13 引入了错误包装机制,让错误可以形成链式结构:

go
import (
    "errors"
    "fmt"
)

var ErrDatabase = errors.New("database error")

func queryUser(id int) (*User, error) {
    err := db.QueryRow("SELECT * FROM users WHERE id = ?", id).Scan(&user)
    if err != nil {
        // 使用 %w 包装原始错误(保留错误链)
        return nil, fmt.Errorf("queryUser id=%d: %w", id, ErrDatabase)
    }
    return &user, nil
}

func getUser(id int) (*User, error) {
    user, err := queryUser(id)
    if err != nil {
        // 继续向上包装
        return nil, fmt.Errorf("getUser: %w", err)
    }
    return user, nil
}

func main() {
    _, err := getUser(42)
    
    // errors.Is 会递归检查整个错误链
    if errors.Is(err, ErrDatabase) {
        fmt.Println("是数据库错误")
    }
    
    // 打印完整错误链
    fmt.Println(err)
    // 输出: getUser: queryUser id=42: database error
}

%w vs %v 的区别

go
// %w - 包装错误,errors.Is/As 可以解包
err1 := fmt.Errorf("操作失败: %w", ErrNotFound)
fmt.Println(errors.Is(err1, ErrNotFound)) // true

// %v - 仅格式化字符串,不保留错误链
err2 := fmt.Errorf("操作失败: %v", ErrNotFound)
fmt.Println(errors.Is(err2, ErrNotFound)) // false

六、errors.Is 与 errors.As 的使用

go
// errors.Is:检查错误链中是否含有目标错误
func checkIs() {
    err := fmt.Errorf("layer 1: %w", fmt.Errorf("layer 2: %w", ErrNotFound))
    
    fmt.Println(errors.Is(err, ErrNotFound)) // true,会递归解包
}

// errors.As:提取错误链中特定类型的错误
func checkAs() {
    err := fmt.Errorf("wrap: %w", &ValidationError{Field: "email", Code: 400})
    
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("Code: %d\n", ve.Code) // Code: 400
    }
}

// errors.Unwrap:手动获取包装的下层错误
func checkUnwrap() {
    inner := ErrNotFound
    outer := fmt.Errorf("outer: %w", inner)
    
    unwrapped := errors.Unwrap(outer)
    fmt.Println(unwrapped == inner) // true
}

七、生产级错误处理模式

7.1 错误添加堆栈信息

标准库的 error 不携带堆栈,推荐使用 github.com/pkg/errors 或手动记录:

go
import pkgerrors "github.com/pkg/errors"

func readConfig(path string) error {
    f, err := os.Open(path)
    if err != nil {
        // WithStack 添加当前调用栈
        return pkgerrors.WithStack(err)
        // 或者 Wrap 同时添加消息和栈
        // return pkgerrors.Wrap(err, "read config failed")
    }
    defer f.Close()
    // ...
    return nil
}

func main() {
    err := readConfig("config.yaml")
    if err != nil {
        // 打印带堆栈的错误
        fmt.Printf("%+v\n", err)
    }
}

7.2 HTTP 层统一错误响应

go
// 定义业务错误码
type AppError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Err     error  `json:"-"` // 内部原始错误,不暴露给客户端
}

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

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

// 预定义常用错误
func NewNotFoundError(msg string, err error) *AppError {
    return &AppError{Code: 404, Message: msg, Err: err}
}

func NewInternalError(err error) *AppError {
    return &AppError{Code: 500, Message: "内部服务器错误", Err: err}
}

// Gin 中间件统一处理
func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) == 0 {
            return
        }
        
        err := c.Errors.Last().Err
        var appErr *AppError
        if errors.As(err, &appErr) {
            c.JSON(appErr.Code, gin.H{
                "code":    appErr.Code,
                "message": appErr.Message,
            })
        } else {
            // 未知错误,返回 500
            c.JSON(500, gin.H{
                "code":    500,
                "message": "内部服务器错误",
            })
        }
    }
}

// 路由处理器
func getUserHandler(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.Error(NewNotFoundError("用户不存在", err))
        return
    }
    
    user, err := userService.GetUser(id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            c.Error(NewNotFoundError("用户不存在", err))
        } else {
            c.Error(NewInternalError(err))
        }
        return
    }
    
    c.JSON(200, user)
}

7.3 错误日志分级记录

go
import "go.uber.org/zap"

type Service struct {
    logger *zap.Logger
}

func (s *Service) ProcessOrder(orderID string) error {
    order, err := s.repo.GetOrder(orderID)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            // 业务正常场景,Info 级别
            s.logger.Info("订单不存在", zap.String("orderID", orderID))
            return fmt.Errorf("ProcessOrder: %w", err)
        }
        // 意外错误,Error 级别
        s.logger.Error("查询订单失败",
            zap.String("orderID", orderID),
            zap.Error(err),
        )
        return fmt.Errorf("ProcessOrder: %w", err)
    }
    
    if err := s.payment.Charge(order); err != nil {
        s.logger.Error("支付失败",
            zap.String("orderID", orderID),
            zap.Float64("amount", order.Amount),
            zap.Error(err),
        )
        return fmt.Errorf("ProcessOrder charge: %w", err)
    }
    
    return nil
}

八、常见反模式

❌ 吞掉错误

go
// 错误:忽略返回的 error
_ = os.Remove(tmpFile)  

// 正确:记录或处理
if err := os.Remove(tmpFile); err != nil {
    log.Printf("清理临时文件失败: %v", err)
}

❌ 重复打印错误

go
// 错误:每一层都打印日志,导致日志重复
func service() error {
    err := repo.Query()
    if err != nil {
        log.Printf("repo error: %v", err) // 第一次打印
        return err
    }
    return nil
}

func handler() {
    err := service()
    if err != nil {
        log.Printf("service error: %v", err) // 第二次打印,重复!
    }
}

// 正确:只在最顶层(入口)打印日志,中间层只包装和传递
func service() error {
    err := repo.Query()
    if err != nil {
        return fmt.Errorf("service.Query: %w", err) // 只包装,不打印
    }
    return nil
}

func handler() {
    err := service()
    if err != nil {
        log.Printf("handler error: %v", err) // 只在顶层打印一次
    }
}

❌ 过于宽泛的错误信息

go
// 错误:信息不足,无法定位问题
return errors.New("error occurred")

// 正确:包含操作、参数、原因
return fmt.Errorf("getUser(id=%d): %w", id, ErrNotFound)

❌ panic 代替 error

go
// 错误:用 panic 处理可预期的业务错误
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero") // 不应该
    }
    return a / b
}

// 正确:返回 error
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

panic 的合理使用场景:程序初始化时检测到无法恢复的配置错误(如数据库连接失败),或者明确的编程错误(如传入了 nil 的必须参数)。

九、errors 包完整 API

go
import "errors"

// 创建简单错误
err := errors.New("something went wrong")

// 检查错误链(递归 Unwrap)
ok := errors.Is(err, target)

// 提取错误链中的特定类型
var target *MyError
ok := errors.As(err, &target)

// 手动解包一层
inner := errors.Unwrap(err)

// Go 1.20+:Join 多个错误
combined := errors.Join(err1, err2, err3)
// errors.Is(combined, err1) == true

十、总结

场景推荐方案
简单错误errors.New
带格式的错误fmt.Errorf("%w", err)
可比较的错误Sentinel error(var ErrXxx = errors.New(...)
需要额外字段自定义错误类型(实现 error 接口)
检查错误类型errors.Is / errors.As
HTTP 层统一响应自定义 AppError + 中间件
错误日志只在最顶层记录,中间层只包装

Go 的错误处理看似繁琐,但显式的错误传递让代码流程更透明、更健壮。掌握这些模式,你的 Go 代码将更加专业。

上次更新于: