Skip to content
Go
errors & log — 错误处理与日志记录

errors & log — 错误处理与日志记录

Go 的错误处理哲学是:错误是值,应该被显式处理。Go 不使用异常(try/catch),而是使用多返回值。


1. errors 包 — 错误创建与处理

1.1 创建错误

// 简单错误(静态字符串)
err := errors.New("something went wrong")

// 格式化错误
err := fmt.Errorf("user %s not found", username)

// Go 1.13+: 错误包装(关键特性)
err := fmt.Errorf("failed to process: %w", originalErr)

🔬 深入原理%w(小写 w)是 Go 1.13 引入的特殊动词,它不会在格式化后的消息字符串中体现 %w,而是创建一个包装错误errors.Iserrors.As 会沿着包装链解包查找。

1.2 errors.Is vs errors.As — 何时用哪个?

函数 用场景 比较方式
errors.Is(err, target) 判断错误"是否等于"某个哨兵错误(包括包装链) == 比较
errors.As(err, &target) 从包装链中"提取"某个类型的错误 类型断言
// errors.Is — 判断是否为 io.EOF(可能在多层包装内)
if errors.Is(err, io.EOF) {
    // 正常结束
}

// errors.As — 获取具体的错误类型以读取字段
var pathError *os.PathError
if errors.As(err, &pathError) {
    fmt.Println("出问题的路径:", pathError.Path)
}

🚨 陷阱

  • errors.As 的第二个参数必须是指向错误类型的指针的指针*MyError**MyError),即 var target *MyError; errors.As(err, &target)
  • 不要用 == 直接比较包装过的错误 — fmt.Errorf("... %w", ErrX) 不等于 ErrX,但 errors.Is 能识别。

1.3 自定义错误类型

// 方案 A:简单哨兵错误
var ErrNotFound = errors.New("not found")

// 方案 B:带字段的错误类型
type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

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

// 方案 C:包装底层错误 + 添加上下文
func getUser(id string) (*User, error) {
    user, err := db.Find(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, fmt.Errorf("get user %s: %w", id, ErrNotFound)
        }
        return nil, fmt.Errorf("get user %s: %w", id, err)
    }
    return user, nil
}

💡 最佳实践

  • 哨兵错误变量名以 Err 开头(ErrNotFoundErrTimeout)。
  • 自定义错误类型名以 Error 结尾。
  • 包装时用 %w 保留原错误的类型信息(让上层可以用 Is/As 判断)。

1.4 errors.Unwrap 与错误链

// 手动解包
for err != nil {
    fmt.Println(err.Error())
    err = errors.Unwrap(err)
}

// Go 1.20+: 支持多重错误
err1 := errors.New("err1")
err2 := errors.New("err2")
combined := errors.Join(err1, err2)
fmt.Println(combined) // "err1\nerr2"
// errors.Is(combined, err1) → true
// errors.Is(combined, err2) → true

🔬 深入原理:Go 1.20 的 errors.Join 返回的复合错误实现了 Unwrap() []error,支持一个错误包含多个底层错误。errors.Is 会对每个子错误进行判断。


2. log 包 — 标准日志

2.1 基础使用

log.Print("message")
log.Printf("formatted %s", "msg")
log.Fatal("critical")  // os.Exit(1) — defer 不执行!
log.Panic("fatal")     // panic() — defer 执行

🚨 陷阱log.Fatal 调用 os.Exit不会执行 defer。如果要保证清理资源,用 log.Print + os.Exitreturn

2.2 日志标志 (Flags)

log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
// 输出:[2024/01/15 10:30:00 server.go:42] message
标志 效果
Ldate 2009/01/23
Ltime 01:23:23
Lmicroseconds 01:23:23.123123
Llongfile /full/path/file.go:23
Lshortfile file.go:23
LUTC 使用 UTC 时间
Lmsgprefix 前缀放在消息前而非日志行首

2.3 自定义 Logger

// 创建不同的 logger 用于不同级别
var (
    Info  = log.New(os.Stdout, "[INFO] ", log.LstdFlags)
    Error = log.New(os.Stderr, "[ERROR] ", log.LstdFlags|log.Lshortfile)
)

2.4 写入文件 + 控制台

file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
multi := io.MultiWriter(file, os.Stdout)
log.SetOutput(multi)

3. 🔬 log/slog — 结构化日志 (Go 1.21+)

slog 是 Go 1.21 引入的结构化日志包,目标是替代 log 成为事实标准。核心思想:日志不再是字符串,而是键值对的集合

3.1 两种 Handler

Handler 输出格式 场景
slog.NewTextHandler(os.Stdout, nil) 人类可读的键值格式 开发环境
slog.NewJSONHandler(os.Stdout, nil) JSON 生产环境(被日志采集系统解析)
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

logger.Info("user logged in",
    "user_id", 123,
    "ip", "192.168.1.1",
    "duration", 150*time.Millisecond,
)
// 输出:{"time":"...","level":"INFO","msg":"user logged in","user_id":123,"ip":"192.168.1.1","duration":"150ms"}

性能提示slog 的键值对是延迟求值的,slog.Stringslog.Int 直接传值避免 boxing,比 ...any 快很多。

3.2 带 Context 的日志(链路追踪)

logger.InfoContext(ctx, "processing request",
    slog.String("method", "GET"),
    slog.String("path", "/api/users"),
)

InfoContext 会自动从 ctx 中提取关联的值(通过 slog.HandlerOptions.AddSource 或自定义 handler 实现)。

3.3 日志级别

slog.LevelDebug  // -4
slog.LevelInfo   // 0(默认)
slog.LevelWarn   // 4
slog.LevelError  // 8

设置级别:

handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
    Level: slog.LevelWarn, // 只记录 Warn 及以上
})

4. 错误处理最佳实践总结

  1. 函数返回 error,而不是 (bool, error) — 只靠 error 判断失败。
  2. errors.Is 替代 == 判断哨兵错误
  3. errors.As 提取类型错误
  4. %w 包装底层错误,保留错误链。
  5. 不要吞没错误_ = doSomething() 写成 if err := doSomething(); err != nil { return err }
  6. 不要重复包装:如果调用方已经包装,被调用方返回原始错误即可。
  7. 哨兵错误应该是包级别的导出变量,不要每次 errors.New