
Go 标准库的 log.Logger 默认记录封装函数自身的行号;通过直接调用 Logger.Output(calldepth, msg) 并设置 calldepth = 2,可跳过包装函数帧,精准定位到真实调用位置。
go 标准库的 log.logger 默认记录封装函数自身的行号;通过直接调用 `logger.output(calldepth, msg)` 并设置 `calldepth = 2`,可跳过包装函数帧,精准定位到真实调用位置。
在 Go 日志实践中,启用 log.Lshortfile(或 log.Llongfile)标志后,日志会自动附加调用点的文件名与行号(如 main.go:23),极大提升调试效率。但一旦对 log.Printf 等方法进行简单封装(例如定义 info()、warn() 等便捷函数),默认行为就会失效——日志显示的是封装函数内部 Printf 的行号,而非业务代码中调用 info() 的那一行。
根本原因在于:log.Logger 的公开方法(如 Printf、Println)内部已硬编码了 calldepth = 2(即跳过自身 + 运行时框架帧),而该深度无法通过公开 API 调整。因此,绕过这些方法、直接调用底层 Logger.Output() 是唯一可靠且标准的解决方案。
Output(calldepth, message) 接收两个参数:
-
calldepth:表示从当前栈帧向上跳过的函数调用层数,用于定位日志“真正来源”; -
message:待输出的日志内容(无需手动加换行符,log包会自动处理)。
对于一层封装函数(如 info()),调用链为:业务代码 → info() → myLogger.Output()
要让日志显示业务代码行号,需跳过 info() 和 Output() 两层,故 calldepth = 2。
✅ 正确封装示例:
package main
import (
"fmt"
"log"
"os"
)
var myLogger = log.New(os.Stdout, "[my] ", log.Lshortfile)
func info(pattern string, args ...interface{}) {
myLogger.Output(2, fmt.Sprintf(pattern, args...))
}
func main() {
log.SetFlags(log.Lshortfile)
log.Println("direct log") // 输出: main.go:16: direct log
info("wrapped log %s", "ok") // 输出: main.go:17: [my] wrapped log ok
}⚠️ 注意事项:
-
calldepth值需严格匹配封装层级:单层函数用2;若info()再被另一层函数调用(如safeInfo()),则需设为3; - 不要手动拼接
\n——log.Logger对不以换行结尾的消息会自动补"\n",重复添加会导致空行; - 若需支持
log.Ldate、log.Ltime等其他标志,Output()同样兼容,无需额外处理; - 此方案完全复用标准库逻辑,零依赖、无反射、性能无损耗。
总结:避免封装 Printf 等高层方法,转而使用 Logger.Output() 并显式控制 calldepth,是 Go 中实现“透明日志封装”的标准实践。它既保持了日志溯源能力,又提供了干净的 API 抽象,是构建可维护日志接口的基石。

















