recover必须写在defer函数里才能捕获panic;单独调用无效,需在可能panic的代码前用defer包裹recover,且每个goroutine须独立加recover并只做安全清理。

recover 必须写在 defer 函数里,不能单独调用
文件下载任务中一旦发生 panic(比如向已关闭的 http.ResponseWriter 写入、空指针解引用、切片越界),想捕获就必须靠 defer + recover() 组合。直接在函数体里写 recover() 永远返回 nil,毫无作用。
常见错误写法:
func downloadHandler(w http.ResponseWriter, r *http.Request) {
recover() // ❌ 无效,永远不生效
panic("write to closed response")
}
正确姿势是把 recover() 包进 defer 的匿名函数里,且必须放在可能 panic 的代码之前:
-
defer语句要写在http.ServeFile、io.Copy或自定义下载逻辑之前 - 多个
defer按后进先出执行,确保最内层的defer负责recover - 不要依赖外层函数的
defer去捕获子 goroutine 里的 panic —— 下载逻辑若起新 goroutine,得各自加defer
HTTP handler 中 recover 后必须立即清理资源
文件下载常涉及打开本地文件、设置响应头、调用 io.Copy,这些操作一旦 panic,recover() 只能阻止崩溃,但状态可能已损坏:
- 已调用
w.Header().Set()?没问题,header 还没发出去 - 已调用
f, _ := os.Open(...)但没defer f.Close()?文件句柄泄漏 - 已向
w写了部分响应体?无法撤回,客户端可能收到截断内容
所以 recover 块里优先做三件事:
- 检查并关闭已打开的文件(
if f != nil && !f.closed { f.Close() }) - 记录 panic 值(用
fmt.Sprint(r),别直接当error用) - 显式返回,不继续执行后续下载逻辑(比如不再调用
http.Error或重试)
并发下载任务中每个 goroutine 都要独立加 recover
如果下载服务用 errgroup.Group 或裸 go func() 并发处理多个文件,主线程的 defer+recover 对它们完全无效。Go 的 panic 是 goroutine 局部的。
典型踩坑场景:
- 在 handler 里起
go func() { panic("timeout") }(),外面的defer收不到 - 后台 worker 没包
recover,panic 后静默退出,连接卡死、临时文件残留
推荐做法:
- 用
errgroup.Group.Go()封装,它内部已自动recover - 或封装一个
goSafe()工具函数,统一注入defer func(){ recover() }() - 避免在 goroutine 入口外漏写
defer,尤其注意循环启动 goroutine 时每一趟都要加
recover 后别复用原变量,状态很可能已不可信
文件下载中常见 panic 场景:向已 close 的 io.WriteCloser 写入、os.Open 返回 nil 后直接解引用、http.Request.URL 为 nil 时取 Path。这些 panic 发生后,相关对象的状态已处于未定义边缘。
例如:
defer func() {
if r := recover(); r != nil {
log.Printf("panic: %v", r)
// ❌ 危险:f 可能已损坏,再调用 f.Stat() 可能二次 panic
if f != nil {
f.Stat() // 不要这么做
}
}
}()
安全做法是:
- 只做确定安全的操作:关文件、释放锁、打日志
- 避免调用任何依赖当前上下文状态的函数(比如再读 map、再写 channel、再查数据库)
- 如需降级,应走显式错误分支(
if err != nil),而非靠recover“硬撑”
真正难处理的不是 panic 本身,而是 panic 后你不知道哪些变量还能信、哪些已失效——宁可早 return,也别赌状态。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











