defer f.Close() 必须在 err 检查之后执行,否则 f 可能为 nil 导致 panic;循环中需用闭包限制 defer 作用域以防资源泄漏;多资源释放须按依赖逆序 defer,确保正确关闭顺序。

defer f.Close() 必须在 err 检查之后执行
打开文件或连接失败时,f 可能是 nil,直接 defer f.Close() 会 panic:panic: runtime error: invalid memory address or nil pointer dereference。必须先确认 err == nil 再注册 defer。
- 正确写法:
f, err := os.Open("x.txt"); if err != nil { return err }; defer f.Close() - 错误写法:
f, err := os.Open("x.txt"); defer f.Close(); if err != nil { return err }(此时f可能为nil) - 对
net.Conn、http.Response.Body、zip.ReadCloser等同理:必须先判错,再 defer
循环中不能直接 defer 多个资源
在 for 循环里反复打开文件或连接时,如果写成 defer conn.Close(),所有 defer 都会累积到外层函数返回时才执行,且都捕获的是最后一次迭代的 conn 值——前面打开的资源全部泄漏。
- 典型错误现象:
for _, url := range urls { resp, _ := http.Get(url); defer resp.Body.Close() }→ 只关了最后一个resp.Body,其余 fd 全泄漏 - 正确做法:用匿名函数立即执行并传参,把
defer限制在局部作用域内:func(r io.ReadCloser) { defer r.Close() }(resp.Body) - 更清晰写法:
resp, err := http.Get(url); if err != nil { continue }; defer func(r io.ReadCloser) { r.Close() }(resp.Body)
多个资源释放要注意关闭顺序
defer 按 LIFO(后进先出)顺序执行,这点在有依赖关系的资源上很关键。比如先打开文件再获取锁,那应该先释放锁再关闭文件;但若 defer 顺序写反,就可能在锁还持有时尝试 close 文件(某些系统会阻塞或报错)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 例如数据库事务中:
tx, _ := db.Begin(); defer tx.Commit()和stmt, _ := tx.Prepare(...); defer stmt.Close()—— 后者必须在前者之前 defer,否则Commit()可能因 stmt 未关而失败 - 常见组合:先
defer file.Close(),再defer mutex.Unlock(),确保解锁前文件已处理完毕 - 不依赖顺序的场景(如独立文件),可连续 defer,Go 会自动逆序执行
t.Cleanup() 是测试中资源释放的首选
在 go test 中,defer 属于函数级,而 t.Cleanup() 是测试级:它注册的函数会在整个测试函数(包括子测试)结束时运行,且即使 t.Fatal() 或 panic 也保证执行。
- 适用场景:
os.CreateTemp创建的临时文件、net.Listen占用的端口、启动的 mock HTTP server - 写法:
tmpfile, _ := os.CreateTemp("", "test-*.txt"); t.Cleanup(func() { os.Remove(tmpfile.Name()) }) - 注意:
t.Cleanup()不替代defer,它只在测试上下文中生效;生产代码仍要用defer
真正容易被忽略的是变量捕获时机和作用域嵌套深度——defer 看似简单,但一旦混入循环、闭包、多层函数调用,参数求值点和执行点就极易错位。别依赖“写了 defer 就安全”这种直觉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










