Go标准库log.Printf默认不带行号,需显式设置log.Lshortfile或log.Llongfile标志;封装日志函数时需用log.New创建新logger并避免调用栈偏移,否则行号指向封装层而非调用处。

log.Printf 默认不带行号,得手动配置
Go 标准库 log 包的 Printf、Println 等函数默认只输出时间 + 内容,不包含文件名和行号。这不是 bug,是设计如此——Go 希望日志行为显式可控,避免隐式开销。
要加行号,必须启用 log.Lshortfile 或 log.Llongfile 标志:
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("hello") // 输出类似:2024/06/10 15:23:41 main.go:12: hello
-
log.Lshortfile显示文件名:行号,不含路径前缀,推荐日常使用 -
log.Llongfile显示完整绝对路径,调试时偶尔有用,但日志体积大、可读性差 - 别漏掉
log.LstdFlags(含时间戳),否则连时间都没了
自定义 logger 才能稳定携带行号
直接用 log.Printf 没问题,但一旦封装成自己的函数(比如 Debugf),行号就变成封装函数内部的行号,不是调用处的 —— 这是 Go 日志最常踩的坑。
原因:log 包通过 runtime.Caller 获取调用栈,默认查第 2 层(log.Printf → 实际调用点),但如果你加了一层包装,它就停在你包装函数里了。
解决办法:用 log.New 创建新 logger,并传入正确的调用深度:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func NewLogger() *log.Logger {
return log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile)
}
// 然后全局用这个 logger,不要自己写 printf 包装函数
logger := NewLogger()
logger.Printf("user id: %d", uid) // 行号正确指向这行
- 绝对不要写
func Debugf(format string, v ...interface{}) { log.Printf(...) }这类函数 - 如果真要封装,必须显式调用
runtime.Caller(1)并手动拼接前缀,成本高且易错 - 第三方库如
zerolog、zap默认支持 Caller Skip,但标准库不提供该接口
log.Lshortfile 在 CGO 或 goroutine 中可能不准
绝大多数情况没问题,但在以下场景中,log.Lshortfile 可能返回错误的文件/行号:
- CGO 调用 C 函数后再回 Go 打印日志,调用栈被截断,
runtime.Caller可能返回 C 的符号或 fallback 到系统帧 - 某些 goroutine 启动方式(如
go func() { ... }()匿名函数内直接调用)在极老 Go 版本( - 交叉编译或 strip 二进制后,部分调试信息丢失,
shortfile可能退化为????:0
验证方法:在可疑位置加一句 log.SetFlags(log.LstdFlags | log.Llongfile) 对比输出,看是否一致。
生产环境慎用 Lshortfile
虽然方便,但 log.Lshortfile 每次打印都要调用 runtime.Caller + 字符串拼接,实测比无标志日志慢 3–5 倍(尤其高频日志场景)。
线上服务若每秒打几百条以上日志,建议:
- 开发/测试环境开启
log.Lshortfile - 生产环境关闭,靠结构化日志字段(如
trace_id、span_id)和集中式日志系统(Loki / ELK)定位问题 - 真需要行号时,用
debug.PrintStack()或 panic 后 recover 捕获栈,而不是常规日志
行号只是辅助手段,真正的可观测性依赖上下文字段和链路追踪,而不是日志里多几个字符。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










