recover不能防止panic但可避免程序崩溃,关键在于在i/o边界层(如每个zip文件解压流程)精准捕获、记录panic、返回error并释放资源,同时必须前置防御如路径清洗、大小限制和格式校验。

recover 不能防止压缩/解压过程中的 panic,但能避免整个程序崩溃;关键不是“用不用 recover”,而是“在哪一层、捕获什么、怎么处理”。
为什么压缩解压逻辑容易 panic
archive/zip 和 compress/gzip 本身不主动 panic,但下游操作极易触发:
-
os.Open文件不存在或权限不足 →panic: runtime error: invalid memory address(若误解包 nil *os.File) -
file.Open()调用在*zip.File上,而该条目是目录(f.IsDir()为 true)→ 返回 nil reader,后续io.Copy时对 nil 调用Read→ panic - 路径拼接未校验,
filepath.Join(dest, f.Name)生成/etc/passwd→os.Create失败后若忽略 error 并继续写入,可能 panic 或静默覆盖 - 大文件解压时内存耗尽,
runtime.GC()无法及时回收,make([]byte, size)分配失败 → panic
recover 应该放在哪一层
不要在 gzip.Write 或 zip.Writer.CreateHeader 外层直接 defer recover —— 这类调用本身几乎不会 panic,捕获不到真正问题。真正需要保护的是 I/O 边界:
- 对每个
*zip.File解压时,用独立的defer func(){ recover() }()包裹其完整流程(open → create → copy) - 压缩时,对每个源文件的
os.Open+io.Copy流程单独加 recover,避免一个文件出错导致整个 ZIP 中断 - HTTP handler 中处理上传 ZIP/GZ 时,在顶层加 recover,防止恶意输入(如超长路径、嵌套 100 层
../)触发栈溢出或系统调用 panic
错误做法:defer recover() 放在 UnzipAll() 函数开头 —— 它捕获不到子函数里提前 return 的 error,也掩盖了本该由调用方处理的业务错误。
捕获后该做什么,而不是“吞掉”
recover 后必须做三件事,缺一不可:
- 记录原始 panic 值:
fmt.Printf("panic in %s: %+v", f.Name, r),否则日志里只剩 “interface {}” - 显式返回 error 或标记失败条目:比如往
[]error切片 append 一条fmt.Errorf("failed on %s: %v", f.Name, r) - 确保资源释放:即使 panic 了,也要 close 已打开的
*os.File或gzip.Reader,否则 fd 泄漏
示例关键片段:
for _, f := range zr.File {
defer func(f *zip.File) {
if r := recover(); r != nil {
// 记录
log.Printf("panic extracting %s: %v", f.Name, r)
// 标记失败(假设 errors 是 []error)
errors = append(errors, fmt.Errorf("extract panic: %s: %v", f.Name, r))
}
}(f)
<pre class="brush:php;toolbar:false;">rc, err := f.Open()
if err != nil {
errors = append(errors, err)
continue
}
defer rc.Close() // 即使后面 panic,这里仍会执行
dst, err := os.Create(filepath.Join(dest, cleanPath))
if err != nil {
errors = append(errors, err)
continue
}
defer dst.Close()
_, err = io.Copy(dst, rc) // 这里可能 panic(如 dst 是 /dev/full)
if err != nil {
errors = append(errors, err)
}}
比 recover 更重要的事:提前防御
recover 是兜底,不是替代检查。以下动作必须在 defer/recover 之前完成:
- 解压前对每个
f.Name调用filepath.Clean(),再检查是否以".."开头或含"/"开头 - gzip 解压前验证 header:
bytes.HasPrefix(data, []byte{0x1f, 0x8b}),长度直接跳过 - zip 解压时,对空目录条目(
f.IsDir()为 true)跳过f.Open(),直接os.MkdirAll - 限制单个文件解压大小(如
f.UncompressedSize64 ),超限直接报错,不进 recover 流程
没做这些就加 recover,等于给定时炸弹装个喇叭——响了你能听见,但房子早塌了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











