go错误处理禁用log.fatal,应原路返回错误由上层统一决策;lumberjack需配合同步机制防日志丢失;可观测日志须结构化、分级隔离,上线前必验高并发日志边界。

Go 错误处理别用 log.Fatal 替代返回错误
线上服务一旦触发 log.Fatal,进程直接退出,Kubernetes 会反复拉起,形成“崩溃-重启”循环,掩盖真实错误路径。它只适合命令行工具的终态失败,不适合 HTTP handler、gRPC server 或后台 worker。
正确做法是把错误原路返回,由上层统一决策:重试、降级、告警或记录后继续。比如 HTTP handler 中:
func handleUser(w http.ResponseWriter, r *http.Request) {
u, err := getUserByID(r.URL.Query().Get("id"))
if err != nil {
// ❌ log.Fatal("failed to get user: ", err)
// ✅ 返回 HTTP 状态 + 结构化错误日志
log.Printf("getUserByID failed: user_id=%s, error=%v", r.URL.Query().Get("id"), err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(u)
}
-
log.Fatal和os.Exit(1)本质相同,绕过 defer、panic 恢复和 graceful shutdown - HTTP handler / gRPC method / cron job 的入口函数,必须把 error 当作一等公民返回,而不是吞掉或 fatal
- 第三方库(如
database/sql)返回的error绝大多数可重试,直接log.Fatal会把连接池抖动变成服务雪崩
日志滚动不能只靠 lumberjack 配置文件切分
lumberjack.Logger 是 Go 生态最常用的日志滚动方案,但仅设 MaxSize 或 MaxAge 不足以应对高并发写入场景——它不保证原子性,多 goroutine 同时写可能丢日志、损坏归档文件。
关键要配合日志库的同步写入能力与外部信号控制:
立即学习“go语言免费学习笔记(深入)”;
lj := &lumberjack.Logger{
Filename: "/var/log/myapp/app.log",
MaxSize: 100, // MB
MaxBackups: 7,
MaxAge: 28, // days
LocalTime: true,
Compress: true,
}
// ✅ 必须用 sync.Mutex 或 sync.Once 包裹 Write 方法(若自定义 Writer)
// ✅ 若用 zap,应搭配 zapcore.Lock,而非直接传 lumberjack 实例
logger := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
zapcore.AddSync(lj), // ← 这里 zapcore 已做 write lock
zap.InfoLevel,
))
- 多个进程(如多实例部署)共用同一日志路径时,
lumberjack无法协调,必须用systemd-journald或集中式日志(Loki/ELK)替代 -
MaxSize单位是 MB,不是字节;MaxAge是天数,不支持小时粒度;这两个参数触发的是「归档」,不是「实时轮转」 - 如果程序需要响应 SIGHUP 重新打开日志(例如运维手动触发 rollover),
lumberjack默认不支持,得自己监听信号并调用lj.Rotate()
大规模日志集成必须区分「可观测日志」和「调试日志」
一个 50 QPS 的微服务,每秒产生 200+ 行日志,其中 95% 是 INFO 级别的请求进出、DB 查询耗时,这些属于可观测日志;剩下 5% 是 DEBUG 级别的变量 dump、嵌套结构体打印,只应在开发或问题定位时开启。
混用会导致两个问题:磁盘 IO 扛不住、日志平台(如 Loki)索引爆炸、查询超时。
- 用不同 logger 实例分离:主 logger(
zap.L().Info)输出结构化可观测日志;debug logger(debugLogger.Debug)默认关闭,通过环境变量DEBUG_LOG=1动态启用 - 可观测日志字段必须固定:至少含
service、trace_id、span_id、level、ts、msg;避免自由字符串拼接(如"user " + u.Name + " logged in") - 不要在
INFO日志里打fmt.Sprintf("%+v", hugeStruct)—— 它会阻塞 goroutine、触发 GC、拖慢 P99 延迟
Go 日志系统上线前必验的三个边界点
很多团队在压测时才发现日志拖垮服务,问题往往出在没验证真实负载下的行为边界。
- 当单次请求产生 >10 条日志时,检查是否用了
defer logger.Info("exit")—— defer 在函数返回前才执行,高并发下大量 defer 堆积会吃光栈空间 - 当日志量突增(如 DB 连接池耗尽,每毫秒打一条 error),确认
lumberjack的Rotate是否阻塞主线程:它内部会 rename + create 新文件,Linux 下虽快,但在 NFS 或低配云盘上可能卡住 100ms+ - 用
go tool trace抓取 30 秒运行时,看sync.Mutex的争用热点 —— 如果日志 writer 是全局单例且没加锁,多 goroutine 写会退化成串行,CPU 利用率虚高但 QPS 掉一半
日志不是越全越好,而是越稳越准。滚动策略失效、错误处理越界、日志级别混淆,这三类问题在线上几乎从不单独出现,常是一起爆发。










