panic时需确保错误发生时runtime能获取调用链,优先用panic(err)或log.Fatalf("%+v", err)(要求err为fmt.Errorf包装),HTTP handler中recover后须用%+v输出以保留行号。

Go panic 时如何看到真实代码行号
默认 panic 打印的堆栈里确实有文件和行号,但如果你用 log.Fatal 或封装了错误却不显式触发堆栈,就只剩 "runtime error: …" 这种没用信息。关键不是“加日志”,而是确保错误发生时 runtime 能拿到调用链。
- 不要用
log.Printf("error: %v", err)后直接os.Exit(1)—— 这等于主动丢掉堆栈 - 想保留位置,就让 panic 真正发生:遇到不可恢复错误时,用
panic(err)或panic(fmt.Sprintf("xxx: %v", err)) - 如果必须用
log.Fatal,改用log.Fatalf("%+v", err)(前提是 err 是用github.com/pkg/errors或 Go 1.13+ 的fmt.Errorf("...: %w", err)包装过的)
用 runtime.Caller 手动打出行号
有些场景不能 panic(比如 HTTP handler 中要返回 500 而非崩溃),就得自己捞位置。别手写 runtime.Caller(1) 就完事 —— 第二个参数是跳过几层,写错就打到 log 包内部去了。
- 在日志封装函数里,固定用
runtime.Caller(2):1 是当前函数,2 是调用你日志函数的地方 - 拿到
file, line, _ := runtime.Caller(2)后,建议截取最后两级路径,避免满屏绝对路径:filepath.Base(file) - 注意:
runtime.Caller在内联函数或某些编译优化下可能不准,Go 1.19+ 可加//go:noinline注释保底
func LogError(err error) {
_, file, line, _ := runtime.Caller(2)
log.Printf("[%s:%d] %v", filepath.Base(file), line, err)
}
Go 1.17+ 推荐:用 errors.WithStack 还是原生 %+v
第三方库如 github.com/pkg/errors 的 WithStack 已基本被原生替代。Go 1.13 引入的 wrapped error + Go 1.17 增强的 %+v 格式符,才是现在最轻量靠谱的方案。
- 用
fmt.Errorf("failed to read config: %w", err)包装错误,保证链路不断 - 日志输出时统一用
log.Printf("%+v", err)—— 它会自动展开所有%w和行号 - 别混用:
pkg/errors的%+v和标准库的%+v行为不一致,同时引入容易漏掉某一层 - 注意:
%+v对非 wrapped error(比如errors.New("xxx"))不显示行号,只对fmt.Errorf创建的才有效
HTTP handler 里怎么不崩又带位置
Web 服务里 panic 会被 recover,但默认 recover 后堆栈就丢了。很多人写了 recover 却只打印 err.Error(),等于前功尽弃。
立即学习“go语言免费学习笔记(深入)”;
- recover 出来的是 interface{},要先断言成
error,再用%+v输出 - 别在 middleware 里简单
log.Println(r)—— 必须用log.Printf("%+v", r) - 如果用了 Gin/Echo 等框架,确认它们的默认 recovery 中间件是否开启 stack trace;Gin 默认关着,得手动设
gin.SetMode(gin.DebugMode)或自定义中间件 - 一个可靠写法:
if r := recover(); r != nil { log.Printf("PANIC in %s: %+v", req.URL.Path, r) }
行号不是日志库给的,是 runtime 在错误创建那一刻记下的。什么时候包装、什么时候格式化、在哪一层 recover —— 这三个点卡不准,再多配置都没用。










