panic时需用runtime.Caller(1)获取文件名和行号,因recover仅返回panic值;debug.Stack()含敏感路径不可返回前端;异步goroutine中panic无法被Gin默认捕获,须手动recover。

panic 时怎么拿到文件名和行号
Go 的 recover() 只能拿到 panic 的值(比如 string 或 error),它本身不带堆栈信息。要获取触发 panic 的具体位置(文件、函数、行号),必须在 recover 后主动调用 debug.Stack() 或 runtime.Caller()。
常见错误是直接打印 err,结果只看到 “internal server error” 或原始 panic 字符串,完全丢失上下文。
-
debug.Stack()返回完整堆栈,适合记录日志,但体积大、含敏感路径,**不能直接返回给前端** -
runtime.Caller(1)只取 panic 发生点的单层信息(推荐用于构造精简错误提示) - 注意参数:传
1是获取 panic 所在函数的位置,0是recover调用处,容易误判
import (
"runtime"
"path/filepath"
)
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// 获取 panic 发生位置
pc, file, line, ok := runtime.Caller(1)
var location string
if ok {
_, filename := filepath.Split(file)
fn := runtime.FuncForPC(pc).Name()
location = filename + ":" + strconv.Itoa(line) + " (" + fn + ")"
} else {
location = "unknown"
}
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"msg": "internal server error",
"location": location, // 仅用于内部排查,生产环境建议关闭
})
}
}()
c.Next()
}
}
为什么 debug.Stack() 在生产环境要脱敏
默认 debug.Stack() 输出包含绝对路径(如 /home/user/project/internal/handler/user.go)、变量名、甚至部分代码片段。攻击者可能利用这些信息推测目录结构、数据库字段或中间件逻辑。
真实踩坑场景:某次上线后,500 错误响应体里暴露了 config/database.yml 路径,被扫描工具抓取并尝试读取 —— 这不是理论风险,是已发生的事故。
- 开发/测试环境可开启
location或完整堆栈,辅助快速定位 - 生产环境必须关闭所有路径、函数名、源码行内容的输出;最多保留“服务端异常,请联系管理员”这类泛化提示
- 若需追踪,应把堆栈写入日志(带 traceID),而非返回 HTTP 响应体
c.Error() 和自定义 error 类型如何联动行号
业务逻辑中用 errors.New() 或 fmt.Errorf() 抛出的 error,默认不带行号。但 Go 1.13+ 的 %w 和包装机制,配合 errors.Unwrap() 可间接支持 —— 关键是用 fmt.Errorf("xxx: %w", err) 包装时,底层会保留原 error 的堆栈(如果它实现了 Unwrap())。
更稳妥的做法是自己实现带行号的 error:
type AppError struct {
Code int
Message string
File string
Line int
}
func NewAppError(code int, msg string) *AppError {
_, file, line, _ := runtime.Caller(1)
return &AppError{
Code: code,
Message: msg,
File: filepath.Base(file),
Line: line,
}
}
func (e *AppError) Error() string {
return fmt.Sprintf("[%s:%d] %s", e.File, e.Line, e.Message)
}
这样在 handler 里 panic(NewAppError(1500, "db query failed")),recover 后就能直接提取 e.File 和 e.Line,无需再解析堆栈字符串。
异步 goroutine 中的 panic 无法被默认 recovery 捕获
Gin 的 recovery 中间件只作用于主 goroutine(即处理 HTTP 请求的那个)。如果你在 go func() {}() 里触发 panic,它不会被任何中间件捕获,进程可能直接崩溃,或者静默丢弃错误。
- 绝对不要在 goroutine 里调用
c.JSON()、c.String()等响应方法 —— 此时c已失效,会 panic - 真要异步执行,必须手动加
defer/recover,且只做日志记录,不操作响应对象 - 想统一收集异步错误?用 channel + 全局 error collector,别试图塞进 Gin 的 error slice
最易被忽略的一点:哪怕你写了完美 recovery,只要有一处 go doSomething(c),就等于留了个无法捕获 panic 的后门。检查所有 go 关键字出现的地方,确认它们是否真的需要并发,以及是否做了 recover。


















