log.Printf 不该直接用于错误日志,因其无时间戳、无调用栈、无级别区分、输出混乱且并发下易丢行混行;应使用 zerolog 等结构化日志库,记录含时间、行号、错误对象及上下文的 JSON 日志,并配 lumberjack 轮转写入。

为什么 log.Printf 不该直接用于错误日志
它默认不带时间戳、无调用栈、无法区分错误级别,且输出到 stderr 或 stdout 后难以归类。线上服务一旦并发打日志,多 goroutine 写同一 os.Stdout 还可能丢行或混行。
实操建议:
- 避免裸用
log.Printf或log.Fatal记录业务错误;它们适合启动失败等极简场景 - 错误日志必须包含:时间、文件/行号、错误原文、上下文字段(如
user_id、req_id) - 优先使用结构化日志库,比如
zerolog或zap,而非标准库log
用 zerolog 记录带上下文的错误日志
zerolog 轻量、零分配、默认 JSON 输出,适合高吞吐服务。关键点是:错误对象要显式传入,不能只打字符串。
示例写法:
立即学习“go语言免费学习笔记(深入)”;
import "github.com/rs/zerolog/log"
func handleRequest(id string) {
err := doSomething(id)
if err != nil {
log.Error().
Str("req_id", id).
Err(err). // ← 关键:用 .Err() 方法传 error 接口
Msg("failed to process request")
return
}
}
注意:
-
.Err(err)会自动展开error的底层信息(包括嵌套错误),比Str("err", err.Error())更可靠 - 若用
log.Output(zerolog.ConsoleWriter{Out: os.Stderr})开发时可读,但上线务必关掉——JSON 格式才利于日志采集(如 filebeat / fluentd) - 不要在
.Msg()里拼接错误消息,否则破坏结构化能力
错误日志落地到文件时的三个硬性要求
本地文件不是终点,而是日志管道的起点。绕过这些容易导致查障失效或磁盘打爆。
必须做到:
- 使用带轮转的 writer,例如
lumberjack.Logger,配置MaxSize(如 100MB)、MaxBackups(如 5)、MaxAge(如 28 天) - 日志文件权限设为
0644,避免因 umask 导致不可读;路径需绝对,如/var/log/myapp/error.log - 禁止用
os.OpenFile(..., os.O_CREATE|os.O_WRONLY|os.O_APPEND)手动管理文件句柄——没锁、没轮转、没 close 控制
典型组合:
import (
"github.com/rs/zerolog"
"gopkg.in/natefinch/lumberjack.v2"
)
w := &lumberjack.Logger{
Filename: "/var/log/myapp/error.log",
MaxSize: 100,
MaxBackups: 5,
MaxAge: 28,
}
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
log := zerolog.New(w).With().Timestamp().Logger()
什么时候该用 panic 而不是记录错误日志
仅当程序处于不可恢复状态时才 panic,比如配置加载失败、数据库连接池初始化失败、监听端口被占用。普通 HTTP 请求处理中的错误(如参数校验失败、DB 查询为空)绝不能 panic。
常见误用:
- 在 HTTP handler 里对
json.Unmarshal错误调panic→ 应返回 400 并记 error 日志 - 用
recover()捕获所有 panic 再打日志 → 掩盖真正问题,且可能引发二次 panic - 把
fmt.Errorf("xxx: %w", err)包装后仍直接panic→ 错误链完整但语义错误
真正该 panic 的信号很窄:进程级依赖缺失、核心全局变量未初始化、unsafe 操作前提不满足。










