Go语言:错误处理的最佳实践

Go 没有 try-catch,错误处理全靠返回值。刚从 Java 转过来时觉得啰嗦,用久了反而觉得这种显式处理挺清晰的。记录一下 Go 错误处理的核心模式。

error 接口

Go 的 error 就是一个接口:

type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都是 error。最简单的创建方式:

import "errors"

err := errors.New("something went wrong")

或者用 fmt.Errorf 带格式化:

err := fmt.Errorf("failed to open file %s: permission denied", filename)

错误包装(Error Wrapping)

Go 1.13 引入了 %w 动词,可以把底层错误包装起来,保留错误链:

file, err := os.Open(path)
if err != nil {
    return fmt.Errorf("loadConfig: %w", err)
}

这样上层代码既能看到 loadConfig: open /etc/app.conf: no such file or directory 这样的完整上下文,又能通过 errors.Is / errors.As 检查底层错误。

errors.Is 和 errors.As

// errors.Is 检查错误链中是否包含特定错误值
if errors.Is(err, os.ErrNotExist) {
    log.Println("file does not exist, using defaults")
    return defaultConfig, nil
}

// errors.As 检查错误链中是否包含特定类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    log.Printf("path error on %s: %v", pathErr.Path, pathErr.Err)
}

不要用 == 比较错误,因为包装后的错误不再和原始值相等。始终用 errors.Is

自定义错误类型

当需要携带结构化信息时,定义自己的错误类型:

type ValidationError struct {
    Field   string
    Message string
}

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

func validateAge(age int) error {
    if age < 0 || age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: fmt.Sprintf("invalid value: %d", age),
        }
    }
    return nil
}

调用方可以用 errors.As 提取:

err := validateAge(-1)
var ve *ValidationError
if errors.As(err, &ve) {
    fmt.Printf("field %s is invalid: %s\n", ve.Field, ve.Message)
}

Sentinel Error

预定义的错误值叫 sentinel error,标准库里有很多:

var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrInternal     = errors.New("internal error")
)

func GetUser(id int) (*User, error) {
    user, ok := db[id]
    if !ok {
        return nil, fmt.Errorf("GetUser(%d): %w", id, ErrNotFound)
    }
    return user, nil
}

sentinel error 应该以 Err 开头,导出给调用方使用。

实践建议

  1. 不要忽略错误——_ = doSomething() 是代码审查红旗
  2. 添加上下文再返回——用 fmt.Errorf("xxx: %w", err) 而不是直接 return err
  3. 只在顶层记日志——中间层只负责包装和传递,避免同一个错误被 log 多次
  4. errors.New 用于定义包级别的 sentinel errorfmt.Errorf 用于运行时动态错误
  5. 不要过度包装——如果上下文已经足够清晰,不需要每一层都加 wrap

Go 的错误处理确实比 try-catch 多写几行代码,但换来的是每个错误路径都被显式考虑,不会出现"忘了 catch 某个异常"的情况。