Go中记录非致命错误应使用log.Printf/log.Println而非log.Fatal/log.Panic,配合slog结构化日志、合理配置时间戳与行号、显式判断error而非滥用recover、善用错误包装、避免高并发日志瓶颈。

Go 中用 log.Printf 或 log.Println 记录错误但不 panic
非致命错误的核心诉求是:日志留下痕迹,程序继续跑。Go 标准库的 log 包天然适合这事——它默认输出到 stderr,不终止进程,也不依赖上下文取消机制。
常见错误是误用 log.Fatal 或 log.Panic,它们会直接调用 os.Exit(1) 或触发 panic,完全违背“不中断流程”目标。
- 用
log.Printf("failed to parse config: %v", err),不是log.Fatalf(...) - 如果需要结构化字段(比如加 traceID),别硬拼字符串,改用
log/slog(Go 1.21+):slog.Warn("parse config failed", "error", err, "trace_id", traceID) - 注意
log默认不带时间戳和文件行号;若需调试定位,提前配置:log.SetFlags(log.LstdFlags | log.Lshortfile)
recover 不是用来兜底所有错误的
recover 只捕获 panic,不能捕获普通 error 返回值。把它当成“非致命错误处理器”是典型误解——这会导致逻辑混乱、掩盖真实控制流。
真实场景中,95% 的非致命错误来自函数返回的 error,比如 http.Client.Do 失败、os.Open 找不到文件、JSON 解析出错。这些都该靠显式判断 + 日志记录,而不是塞进 defer + recover 里。
立即学习“go语言免费学习笔记(深入)”;
- 不要写:
defer func() { if r := recover(); r != nil { log.Printf("recovered: %v", r) } }()来处理 HTTP 请求失败 - 应该写:
resp, err := client.Do(req); if err != nil { log.Printf("HTTP request failed: %v", err); return } -
recover只应在极少数位置使用:比如插件系统、HTTP handler 顶层兜住意外 panic,防止整个服务崩溃
错误包装与分类影响日志可读性
记录错误时只打 err.Error() 往往丢失上下文。比如 os.Open 报 “no such file”,你不知道是配置路径错了,还是用户上传的文件名被截断了。
Go 1.13+ 的错误包装(%w)和 errors.Is/errors.As 不仅用于控制流判断,也直接影响日志信息质量。
- 记录前建议展开链路:
log.Printf("failed to load user profile: %v (wrapped: %+v)", err, errors.Unwrap(err)) - 避免无差别记录整个错误链(尤其含敏感路径或密码),先用
errors.Is(err, os.ErrNotExist)判断类型,再按需打不同日志级别 - 如果用了
slog,直接传 error 值:slog.Error("load profile failed", "error", err),它会自动调用fmt.Formatter接口,展示完整错误链
并发写日志可能丢数据或阻塞
标准 log 是线程安全的,但底层用互斥锁串行化输出。高并发下(比如每秒上千次错误日志),它会成为瓶颈,甚至拖慢主业务逻辑。
这不是理论风险——在压测中见过日志写入延迟从毫秒级升到秒级,导致超时连锁反应。
- 不要在 hot path(如每个 HTTP 请求的中间件)里无条件记 error 日志;先采样或加条件:
if err != nil && !errors.Is(err, context.Canceled) { log.Printf(...) } - 考虑用异步日志库(如
zerolog配合io.MultiWriter+ channel buffer),但要注意:异步意味着崩溃时未 flush 的日志会丢失 - 最稳妥的折中:用
log.SetOutput指向带缓冲的bufio.Writer,例如log.SetOutput(bufio.NewWriterSize(os.Stderr, 64*1024)),并在进程退出前调用log.Output().(*bufio.Writer).Flush()
真正难的不是记下错误,而是让日志既不干扰主流程,又能在出问题时提供足够线索——这两者之间没银弹,只有根据吞吐、可靠性和调试成本做取舍。










