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.Is和errors.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开头(ErrNotFound、ErrTimeout)。- 自定义错误类型名以
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.Exit或return。
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.String和slog.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. 错误处理最佳实践总结
- 函数返回
error,而不是(bool, error)— 只靠 error 判断失败。 - 用
errors.Is替代==判断哨兵错误。 - 用
errors.As提取类型错误。 - 用
%w包装底层错误,保留错误链。 - 不要吞没错误:
_ = doSomething()写成if err := doSomething(); err != nil { return err }。 - 不要重复包装:如果调用方已经包装,被调用方返回原始错误即可。
- 哨兵错误应该是包级别的导出变量,不要每次
errors.New。