Go语言错误处理最佳实践:告别panic滥用与nil陷阱

2026-05-30 码匠 Go语言

Go 语言的错误处理一直是社区讨论的热点。与 Java/C# 的 try-catch 不同,Go 采用显式的 error 返回值机制,这让错误处理更加透明,但也带来了代码冗长的问题。本文总结 Go 错误处理的最佳实践,帮你写出更健壮的 Go 代码。

一、Go 错误处理的基本原则

Go 的核心理念:错误是值,不是异常。函数通过返回 error 告知调用方发生了什么,调用方必须显式处理。

func ReadFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("read file %s: %w", path, err)
    }
    return data, nil
}

关键点:用 fmt.Errorf 包裹错误时加上上下文信息,让调用链中的错误可追溯。

二、常见反模式

1. 滥用 panic

// 错误:用 panic 代替 error
func GetConfig(key string) string {
    val := config[key]
    if val == "" {
        panic(fmt.Sprintf("config %s not found", key))
    }
    return val
}

// 正确:返回 error
func GetConfig(key string) (string, error) {
    val, ok := config[key]
    if !ok {
        return "", fmt.Errorf("config %s not found", key)
    }
    return val, nil
}

规则:panic 只用于真正不可恢复的程序错误(如数组越界、nil 指针),业务逻辑的错误一律用 error。

2. 吞掉错误

// 错误:忽略 error
data, _ := os.ReadFile(path)

// 正确:至少记录日志
data, err := os.ReadFile(path)
if err != nil {
    log.Printf("read file failed: %v", err)
    return err
}

3. 返回 nil 值 + nil error

// 错误:返回 nil 但没有 error
func FindUser(id int) *User {
    // 没找到时返回 nil,但调用方不知道是"没找到"还是"出错了"
}

// 正确:明确区分"没找到"和"出错"
func FindUser(id int) (*User, error) {
    user, err := db.Find(id)
    if err != nil {
        return nil, err  // 出错
    }
    if user == nil {
        return nil, ErrNotFound  // 没找到
    }
    return user, nil
}

三、错误包装与拆包

1. %w 包装(Go 1.13+)

func ReadConfig() (Config, error) {
    data, err := os.ReadFile("config.json")
    if err != nil {
        return Config{}, fmt.Errorf("read config: %w", err)
    }
    // ...
}

// 调用方可以用 errors.Is/As 拆包
if errors.Is(err, os.ErrNotExist) {
    // 处理文件不存在
}

2. errors.Is vs errors.As

// 判断错误类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("路径:", pathErr.Path)
}

// 判断特定错误值
if errors.Is(err, io.EOF) {
    // 文件结束
}

四、自定义错误类型

1. 简单哨兵错误

var (
    ErrNotFound    = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrRateLimited  = errors.New("rate limited")
)

2. 结构化错误

type AppError struct {
    Code    string
    Message string
    Err     error
}

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

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

// 使用
return &AppError{
    Code:    "DB_001",
    Message: "query user failed",
    Err:     err,
}

五、减少重复的 if err != nil

1. 辅助函数封装

func must[T any](val T, err error) T {
    if err != nil {
        panic(err)
    }
    return val
}

// 仅在初始化阶段使用
config := must(LoadConfig("config.json"))

2. 错误处理中间件模式

func HandleError(fn func() error) {
    if err := fn(); err != nil {
        log.Printf("error: %v", err)
        // 统一处理:上报、重试等
    }
}

HandleError(func() error {
    return DoSomething()
})

六、HTTP 服务中的错误处理

func GetUser(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    user, err := service.FindUser(id)
    
    if err != nil {
        switch {
        case errors.Is(err, ErrNotFound):
            http.Error(w, "user not found", http.StatusNotFound)
        case errors.Is(err, ErrUnauthorized):
            http.Error(w, "unauthorized", http.StatusUnauthorized)
        default:
            log.Printf("internal error: %v", err)
            http.Error(w, "internal error", http.StatusInternalServerError)
        }
        return
    }
    
    json.NewEncoder(w).Encode(user)
}

七、goroutine 中的错误处理

func ProcessAll(items []Item) error {
    errCh := make(chan error, len(items))
    
    for _, item := range items {
        go func(it Item) {
            errCh <- process(it)
        }(item)
    }
    
    var errs []error
    for i := 0; i < len(items); i++ {
        if err := <-errCh; err != nil {
            errs = append(errs, err)
        }
    }
    
    if len(errs) > 0 {
        return fmt.Errorf("%d errors occurred, first: %w", len(errs), errs[0])
    }
    return nil
}

八、最佳实践总结

  1. 显式处理错误:不要吞掉、不要用 _ 忽略
  2. 用 %w 包装错误:保留错误链,方便 errors.Is/As
  3. 添加上下文:fmt.Errorf 中加入操作名和参数
  4. 不用 panic:业务逻辑用 error,panic 只用于不可恢复的 bug
  5. 自定义错误类型:让调用方可以精确判断和处理
  6. 统一错误入口:HTTP 服务用中间件统一处理

关于我们

如果你在Go 开发、微服务架构、错误处理设计等方面需要帮助,尊云科技可以为你提供服务。

我们专注于网站建设、APP开发、小程序开发、PHP/Vue/UniApp开发、API接口、AI集成,拥有丰富的项目经验。

联系方式: