Go标准库log默认不显示file:line,需调用log.SetFlags(log.LstdFlags|log.Lshortfile)提前设置;手动封装时应使用runtime.Caller(1)获取调用方位置,并用filepath.Base精简路径。

Go 日志里为什么总看不到 file:line?
因为标准库 log 默认不记录调用位置,它只管输出你给的字符串。哪怕用了 log.Printf,只要没手动传入文件名和行号,日志里就只有消息内容,没有上下文定位能力。
常见错误现象:2024/05/20 14:22:03 failed to open config: permission denied —— 你根本不知道这行日志是从哪个 .go 文件、哪一行打出来的。
真正起作用的是 log.SetFlags 配合 log.Lshortfile 或 log.Llongfile:
log.SetFlags(log.LstdFlags | log.Lshortfile)
注意:必须在任何 log.Print* 调用前设置,否则无效;且该设置是全局的,会影响所有后续标准日志输出。
立即学习“go语言免费学习笔记(深入)”;
用 runtime.Caller 手动提取文件行号的坑
想自己封装带位置的日志函数?别直接写 runtime.Caller(0) —— 那拿到的是你封装函数自身的行号,不是调用方的。
正确做法是跳过当前函数和上层包装函数,通常要跳 2 层:
-
runtime.Caller(0)→ 封装函数内部 -
runtime.Caller(1)→ 调用你封装函数的地方(多数情况够用) -
runtime.Caller(2)→ 再往上一层,比如被defer或中间件触发时更稳妥
示例关键片段:
func Error(msg string) {
_, file, line, ok := runtime.Caller(1)
if !ok {
file = "???"
line = 0
}
log.Printf("%s:%d %s", filepath.Base(file), line, msg)
}
注意 filepath.Base(file),避免日志里出现一长串绝对路径(如 /home/user/project/internal/handler.go),影响可读性。
装饰器模式下如何不丢失原始调用位置
如果你写了类似 WithRecovery 或 WithLogging 的中间件,又希望日志里的 file:line 指向业务代码而非中间件本身,就不能依赖 log.Lshortfile 的自动推导 —— 因为日志发生在中间件函数体内。
可行方案是让装饰器接收一个“日志构造器”或透传 runtime.Caller 的结果:
- 在业务 handler 入口处提前调用
runtime.Caller(1),把file和line作为上下文传入 - 或者让装饰器接受一个
func() (string, int)类型的钩子,在真正需要打日志时才执行获取
不要试图在装饰器内部统一用 Caller(2) —— 调用栈深度会因 defer、goroutine、test 环境而变化,不稳定。
第三方日志库(如 zap)怎么加行号
zap 默认也不带文件行号,但提供了 zap.AddCaller() 开关:
logger := zap.NewExample().WithOptions(zap.AddCaller())
注意两点:
-
zap.AddCaller()只对logger.Info/Error等方法生效,对logger.With(...).Info()中的With不起作用 - 它默认跳过 zap 自身代码,但如果你封装了日志函数,仍需配合
zap.AddCallerSkip(1)手动跳过你的封装层
性能提示:开启 AddCaller 会带来约 10%~15% 的额外开销(源于 runtime.Caller 调用),高频日志场景建议关闭,或仅在 debug 环境启用。
最常被忽略的一点:无论用标准库还是 zap,只要日志最终是通过某一层函数中转的,就必须明确控制 Caller 的跳过层数。这个数字不是固定的,得看你实际调用链有多深 —— 宁可多试几次 Caller(1) 到 Caller(3),也不要凭感觉硬写。










