Go错误处理Skill go-error-handling

这个技能专注于Go编程语言中的错误处理,涵盖错误包装、哨兵错误、自定义错误类型等核心模式,用于构建健壮和可维护的后端应用程序。关键词:Go 错误处理、错误包装、自定义错误、哨兵错误、错误日志、HTTP错误处理。

后端开发 0 次安装 2 次浏览 更新于 3/25/2026

name: go-error-handling user-invocable: false description: 用于Go错误处理,包括错误包装、哨兵错误和自定义错误类型。在Go应用程序中处理错误时使用。 allowed-tools:

  • Bash
  • Read

Go 错误处理

掌握Go的错误处理模式,包括错误包装、哨兵错误、自定义错误类型和errors包,用于构建健壮的应用程序。

基本错误处理

创建和返回错误:

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("除以零")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
        return
    }
    fmt.Println("结果:", result)
}

使用 fmt.Errorf:

func processFile(filename string) error {
    if filename == "" {
        return fmt.Errorf("文件名不能为空")
    }
    // 处理文件...
    return nil
}

错误包装

使用上下文包装错误(Go 1.13+):

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

func readConfig(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("读取配置失败:%w", err)
    }
    return nil
}

func main() {
    err := readConfig("config.json")
    if err != nil {
        fmt.Println(err)
        // 输出:读取配置失败:打开 config.json:没有这样的文件
    }
}

解包错误:

func handleError(err error) {
    // 解包一层
    unwrapped := errors.Unwrap(err)
    if unwrapped != nil {
        fmt.Println("解包后:", unwrapped)
    }

    // 检查链中是否有特定错误
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("文件不存在")
    }
}

哨兵错误

定义和使用哨兵错误:

package main

import (
    "errors"
    "fmt"
)

var (
    ErrNotFound     = errors.New("资源未找到")
    ErrUnauthorized = errors.New("未经授权的访问")
    ErrInvalidInput = errors.New("无效输入")
)

func getUser(id int) (string, error) {
    if id < 0 {
        return "", ErrInvalidInput
    }
    if id == 0 {
        return "", ErrNotFound
    }
    return fmt.Sprintf("用户-%d", id), nil
}

func main() {
    _, err := getUser(0)
    if errors.Is(err, ErrNotFound) {
        fmt.Println("用户未找到")
    }
}

自定义错误类型

实现错误接口:

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s上的验证错误:%s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "年龄",
            Message: "必须为正数",
        }
    }
    if age > 150 {
        return &ValidationError{
            Field:   "年龄",
            Message: "必须小于150",
        }
    }
    return nil
}

func main() {
    err := validateAge(-5)
    if err != nil {
        fmt.Println(err)
    }
}

使用 errors.As 进行类型断言:

func handleValidation(err error) {
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        fmt.Printf("字段 '%s' 失败:%s
",
            validationErr.Field,
            validationErr.Message,
        )
    }
}

多错误处理

收集多个错误:

type MultiError struct {
    Errors []error
}

func (m *MultiError) Error() string {
    if len(m.Errors) == 0 {
        return "没有错误"
    }
    if len(m.Errors) == 1 {
        return m.Errors[0].Error()
    }
    return fmt.Sprintf("发生%d个错误:%v", len(m.Errors), m.Errors)
}

func (m *MultiError) Add(err error) {
    if err != nil {
        m.Errors = append(m.Errors, err)
    }
}

func validateUser(name, email string, age int) error {
    errs := &MultiError{}

    if name == "" {
        errs.Add(errors.New("姓名是必需的"))
    }
    if email == "" {
        errs.Add(errors.New("电子邮件是必需的"))
    }
    if age < 0 {
        errs.Add(errors.New("年龄必须为正数"))
    }

    if len(errs.Errors) > 0 {
        return errs
    }
    return nil
}

恐慌和恢复

何时使用恐慌:

// 恐慌用于不可恢复的错误
func mustConnect(dsn string) *DB {
    db, err := connect(dsn)
    if err != nil {
        panic(fmt.Sprintf("连接到数据库失败:%v", err))
    }
    return db
}

// 从恐慌中恢复
func safeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("恐慌恢复:%v", r)
        }
    }()

    fn()
    return nil
}

错误处理模式

早期返回模式:

func processRequest(id int) error {
    user, err := fetchUser(id)
    if err != nil {
        return fmt.Errorf("获取用户:%w", err)
    }

    if err := validateUser(user); err != nil {
        return fmt.Errorf("验证用户:%w", err)
    }

    if err := saveUser(user); err != nil {
        return fmt.Errorf("保存用户:%w", err)
    }

    return nil
}

错误变量命名:

// 好:具体的错误名称
errDB := connectDB()
if errDB != nil {
    return fmt.Errorf("数据库连接:%w", errDB)
}

errCache := connectCache()
if errCache != nil {
    return fmt.Errorf("缓存连接:%w", errCache)
}

// 避免:到处重用'err'会使调试更困难

pkg/errors 模式(传统)

使用 github.com/pkg/errors:

import (
    "github.com/pkg/errors"
)

func loadConfig() error {
    _, err := os.Open("config.json")
    if err != nil {
        return errors.Wrap(err, "加载配置失败")
    }
    return nil
}

func init() {
    if err := loadConfig(); err != nil {
        // 打印堆栈跟踪
        fmt.Printf("%+v
", err)
    }
}

错误日志记录

结构化错误日志记录:

import (
    "log/slog"
)

func processOrder(orderID string) error {
    order, err := fetchOrder(orderID)
    if err != nil {
        slog.Error("获取订单失败",
            "orderID", orderID,
            "error", err,
        )
        return fmt.Errorf("获取订单 %s:%w", orderID, err)
    }

    if err := validateOrder(order); err != nil {
        slog.Warn("订单验证失败",
            "orderID", orderID,
            "error", err,
        )
        return fmt.Errorf("验证订单:%w", err)
    }

    return nil
}

HTTP 错误处理

处理 HTTP 错误:

import (
    "encoding/json"
    "net/http"
)

type APIError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

func (e *APIError) Error() string {
    return e.Message
}

func writeError(w http.ResponseWriter, err error) {
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        w.WriteHeader(apiErr.Code)
        json.NewEncoder(w).Encode(apiErr)
        return
    }

    // 默认错误
    w.WriteHeader(http.StatusInternalServerError)
    json.NewEncoder(w).Encode(APIError{
        Code:    http.StatusInternalServerError,
        Message: "内部服务器错误",
    })
}

func handler(w http.ResponseWriter, r *http.Request) {
    err := processRequest(r)
    if err != nil {
        writeError(w, err)
        return
    }

    w.WriteHeader(http.StatusOK)
}

错误上下文

向错误添加上下文:

type ContextError struct {
    Op      string // 操作
    Path    string // 文件路径、URL等
    Err     error  // 底层错误
}

func (e *ContextError) Error() string {
    return fmt.Sprintf("%s %s:%v", e.Op, e.Path, e.Err)
}

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

func readFile(path string) error {
    _, err := os.ReadFile(path)
    if err != nil {
        return &ContextError{
            Op:   "读取",
            Path: path,
            Err:  err,
        }
    }
    return nil
}

测试错误情况

测试错误条件:

package main

import (
    "errors"
    "testing"
)

func TestDivideByZero(t *testing.T) {
    _, err := divide(10, 0)
    if err == nil {
        t.Fatal("预期错误,得到nil")
    }

    expected := "除以零"
    if err.Error() != expected {
        t.Errorf("预期 %q,得到 %q", expected, err.Error())
    }
}

func TestErrorWrapping(t *testing.T) {
    err := readConfig("missing.json")
    if err == nil {
        t.Fatal("预期错误")
    }

    if !errors.Is(err, os.ErrNotExist) {
        t.Error("预期包装的 ErrNotExist")
    }
}

func TestCustomError(t *testing.T) {
    err := validateAge(-1)

    var validationErr *ValidationError
    if !errors.As(err, &validationErr) {
        t.Fatal("预期 ValidationError")
    }

    if validationErr.Field != "年龄" {
        t.Errorf("预期字段 '年龄',得到 %q", validationErr.Field)
    }
}

何时使用此技能

使用 go-error-handling 当您需要:

  • 正确处理Go应用程序中的错误
  • 添加错误上下文而不丢失信息
  • 定义领域特定的错误类型
  • 检查特定错误条件
  • 使用额外上下文包装错误
  • 以适当细节记录错误
  • 从HTTP处理器返回错误
  • 彻底测试错误条件
  • 构建错误恢复系统
  • 基于错误类型实现重试逻辑

最佳实践

  • 始终检查错误,从不忽略它们
  • 返回错误而不是记录并继续
  • 使用 fmt.Errorf 和 %w 包装错误
  • 使用 errors.Is 比较哨兵错误
  • 使用 errors.As 进行类型断言
  • 在错误消息中提供上下文
  • 使用自定义错误类型处理领域错误
  • 不在库中使用恐慌,返回错误
  • 以适当级别记录错误
  • 像测试成功路径一样彻底测试错误路径

常见陷阱

  • 使用 _ 赋值忽略错误
  • 不包装错误(丢失上下文)
  • 使用 == 进行错误比较
  • 恐慌而不是返回错误
  • 不处理所有错误情况
  • 创建太多自定义错误类型
  • 错误消息格式不佳
  • 不测试错误条件
  • 在goroutine中吞没错误
  • 错误中没有提供足够上下文

资源