go标准库log包缺乏动态级别开关、多writer支持、结构化字段及trace_id/caller注入能力,因此需自定义轻量稳定日志模块;核心是先定义最小logger接口并用结构体field避免反射开销,再通过bufio.writer和multiwriter保障并发安全与多目标输出,caller仅在warn及以上级别启用且caller(4)精准定位,timestamp用unixmilli()提升性能。

为什么不用 log 包而要自己写模块?
Go 标准库的 log 包够用,但缺几个关键能力:不能按 level 动态开关输出、不支持多 writer(比如同时写文件和 stdout)、没有结构化字段(key=value)、也不方便加 trace_id 或 caller 信息。你真需要这些时,不是“要不要自定义”,而是“怎么让自定义足够轻、足够稳、不侵入业务”。
核心接口设计:先定 Logger 接口再实现
别一上来就写文件写法或 JSON 序列化——先想清楚调用方要什么。标准做法是定义一个最小接口:
type Logger interface {
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
With(fields ...Field) Logger
}
注意两点:With 返回新 Logger 是为了支持链式上下文(比如 request-scoped 日志),Field 用结构体而非 map[string]interface{},避免反射和类型擦除开销:
type Field struct {
Key string
Value interface{}
}
避免踩坑:日志 writer 的并发安全与 flush 控制
常见错误是直接把 os.File 丢给 log.New 然后并发写——os.File.Write 虽然线程安全,但没做缓冲控制,高并发下小日志会频繁 syscall;更糟的是忘记 Close 或 Sync,导致 crash 前最后一段日志丢失。
- 用
bufio.Writer包一层,但必须显式Flush(尤其进程退出前) - 如果写多个目标(如文件 + 网络),用
io.MultiWriter,别自己锁 - 级别过滤放在写之前,而不是 writer 里——否则低 level 日志仍会进 buffer、占内存
Caller 和 timestamp 怎么加才不影响性能?
加 runtime.Caller 很贵,别每次调用都算。折中方案是:只在 debug 模式或 error 级别开启 caller;timestamp 用 time.Now().UnixMilli()(Go 1.17+),比 Format 快 5–10 倍。
示例片段(caller 提取):
if l.withCaller && level >= LevelWarn {
_, file, line, _ := runtime.Caller(4) // 跳过封装层
fields = append(fields, Field{"file", filepath.Base(file)}, Field{"line", line})
}
这里 Caller(4) 的数字很关键:Debug → 封装函数 → 日志方法 → runtime.Caller,数错就拿到错的文件行号。
真正难的不是写完,是压测时发现日志 goroutine 占用 CPU 过高,或 panic 后日志全丢——这些得靠 recover + 异步 fallback writer + ring buffer 控制背压,但那是另一层复杂度了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











