症状
资源曲线呈阶梯状:fd 计数每次迭代加一、中途从不回落,直到撞上限崩溃——清理被“堆叠”而不是“执行”。代码形态一目了然:for 循环里 os.Open 后紧跟 defer f.Close(),这条 defer 绑定的是整个外层函数,一千个路径就是一千个同时打开的文件。mutex 版本失败方式不同但根源相同:循环内 defer mu.Unlock() 意味着迭代之间从不解锁,互斥形同虚设还常直接死锁。生产里泄漏、跑得快的测试毫无察觉,是因为短测试在撞限前就结束了。
诊断思路
先实时观察 /proc/
根因
defer 把清理调度到函数退出而非作用域退出。循环内每轮迭代都向函数的延迟栈追加一次调用,所有文件、连接或锁同时保持打开,直到外层函数最终返回。
修复与验证
把循环体提取成独立函数或方法,defer 绑定到单次迭代;提取也顺带改良错误路径——每轮拥有自己的清理,错误不会搁浅已累积的延迟调用;重型场景用 worker 池,每个 worker 有自己的函数边界,同样的作用域外加有界并发。验证时在低 fd 限制下跑完整批次断言完成,并断言迭代期间句柄计数保持平坦。不建议改成手工 close——那会重新引入 defer 本来要解决的提前 return 泄漏问题,提取函数才是正解。
代码示例
fd count: 3 → 4 → 5 → 6 → ... → 1024 → crash: too many open files
func processAll(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // runs when processAll returns — after the whole loop
handle(f)
}
return nil
}
func processAll(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // bound to this iteration
return handle(f)
}