defer 详解
本教程共 80 篇 · 第 25 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
25. defer 详解
本节目标:彻底搞懂 defer 的执行顺序、参数求值时机,以及 Go 1.14+ 的性能优化,学会用 defer 管理资源。
什么是 defer
defer 语句用于延迟执行一个函数调用,被 defer 的函数会在当前函数返回之前执行。
最经典的用途是资源释放:
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 确保函数返回时关闭文件
// 读取文件内容...
return nil
}
不管函数从哪个 return 返回,甚至 panic 了,f.Close() 都会被执行。
defer 的执行顺序
多个 defer 按后进先出(LIFO)顺序执行,就像栈一样:
func main() {
defer fmt.Println("第一个")
defer fmt.Println("第二个")
defer fmt.Println("第三个")
fmt.Println("正常执行")
}
输出:
正常执行
第三个
第二个
第一个
先注册的最后执行,后注册的最先执行。
为什么是 LIFO
因为资源的获取和释放通常是嵌套的。先打开的文件最后关闭,先获取的锁最后释放:
func process() {
f1, _ := os.Open("file1")
defer f1.Close()
f2, _ := os.Open("file2")
defer f2.Close()
// f2 先关闭,f1 后关闭
}
LIFO 保证了资源的正确释放顺序。
参数立即求值
defer 的参数在 defer 语句执行时就求值,不是在函数真正执行时。
看个例子
func main() {
x := 10
defer fmt.Println("defer 中 x =", x) // 此时 x = 10
x = 20
fmt.Println("正常 x =", x)
}
输出:
正常 x = 20
defer 中 x = 10
defer fmt.Println("defer 中 x =", x) 执行时,x 是 10,这个值就被「固定」了。后面 x = 20 不影响 defer 的参数。
闭包方式延迟求值
如果你想让 defer 在执行时才读取变量的值,用闭包:
func main() {
x := 10
defer func() {
fmt.Println("defer 中 x =", x) // 执行时读取 x
}()
x = 20
fmt.Println("正常 x =", x)
}
输出:
正常 x = 20
defer 中 x = 20
因为 defer 注册的是匿名函数,匿名函数没有参数,x 是在执行时从闭包捕获的。
Tip记住这个区别:
defer fmt.Println(x)— 参数立即求值defer func() { fmt.Println(x) }()— 闭包延迟求值
defer 和返回值
defer 执行时机是 return 之后、函数真正退出之前。它可以修改命名返回值:
func foo() (result int) {
defer func() {
result *= 2 // 修改返回值
}()
return 5
}
func main() {
fmt.Println(foo()) // 10
}
执行流程:
return 5把 5 赋给result- 执行 defer,
result *= 2变成 10 - 函数返回
result,值是 10
Note只有命名返回值能被 defer 修改。匿名返回值不行:
func bar() int {
result := 5
defer func() {
result *= 2 // 修改的是局部变量,不影响返回值
}()
return result // 返回 5,不是 10
}
defer 常见用途
资源释放
func readFile(path string) {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close() // 确保关闭
// 使用文件...
}
锁释放
var mu sync.Mutex
func safeUpdate(m map[string]int, key string, value int) {
mu.Lock()
defer mu.Unlock() // 确保解锁
m[key] = value
}
恢复 panic
func safeRun() {
defer func() {
if r := recover(); r != nil {
fmt.Println("捕获到 panic:", r)
}
}()
panic("出错了")
}
计时器
func timeTrack(start time.Time, name string) {
elapsed := time.Since(start)
fmt.Printf("%s 耗时 %v\n", name, elapsed)
}
func slowFunc() {
defer timeTrack(time.Now(), "slowFunc")
time.Sleep(2 * time.Second)
}
日志记录
func handleRequest(req string) {
defer log.Printf("请求处理完成:%s", req)
log.Printf("开始处理请求:%s", req)
// 处理请求...
}
Go 1.14+ defer 优化
在 Go 1.14 之前,每个 defer 都有一定的性能开销(大约几十纳秒),因为它需要在堆上分配 defer 记录。
Go 1.14 引入了开放编码(Open-coded defer)优化:
- 对于最多 8 个 defer 的函数
- 且 defer 不在循环内
- 编译器会把 defer 内联,几乎零开销
优化后的 defer 性能和直接调用函数差不多:
// Go 1.14+ 优化后,这些 defer 几乎零开销
func fastFunc() {
mu.Lock()
defer mu.Unlock()
f, _ := os.Open("file")
defer f.Close()
// 快速操作...
}
TipGo 1.14 以后,大部分场景下你不需要担心 defer 的性能开销。优先用 defer 保证资源释放的正确性,而不是为了「性能」手动管理。
defer 的注意事项
defer 在循环中
// 不推荐:defer 在循环中,资源会堆积到函数返回才释放
func processFiles(paths []string) {
for _, path := range paths {
f, _ := os.Open(path)
defer f.Close() // 所有文件在函数返回时才关闭
// 处理文件...
}
}
更好的做法是把循环体提取成函数:
func processFiles(paths []string) {
for _, path := range paths {
processFile(path)
}
}
func processFile(path string) {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close() // 每次调用结束后就关闭
// 处理文件...
}
defer 的参数是函数调用
// defer 执行的是 fmt.Println 的返回值?不,defer 延迟执行整个函数调用
defer fmt.Println("done")
// 等价于:在函数返回前执行 fmt.Println("done")
defer 后面必须跟函数调用,不能是表达式:
defer x++ // 报错:defer requires a function call
defer func() { x++ }() // 正确
defer 不能在 return 语句之后
func foo() int {
return 1
defer fmt.Println("不会执行") // 永远不会执行,因为前面已经 return 了
}
defer 必须在 return 之前注册才能执行。
实际示例
数据库事务
func updateUser(db *sql.DB, id int, name string) error {
tx, err := db.Begin()
if err != nil {
return err
}
// 根据是否出错决定提交还是回滚
success := false
defer func() {
if success {
tx.Commit()
} else {
tx.Rollback()
}
}()
_, err = tx.Exec("UPDATE users SET name = ? WHERE id = ?", name, id)
if err != nil {
return err // 触发 Rollback
}
success = true // 标记成功,触发 Commit
return nil
}
HTTP 响应体关闭
func fetchURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close() // 确保关闭响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
WarningHTTP 响应体必须关闭,否则会导致连接泄漏。用 defer 是最安全的方式。
常见问题
defer 执行顺序搞错
func main() {
defer fmt.Println("A")
fmt.Println("B")
defer fmt.Println("C")
}
// 输出:B C A
defer 不是在它声明的位置执行,而是在函数返回前。多个 defer 按 LIFO 顺序。
defer 修改不了匿名返回值
func foo() int {
defer func() {
// 没法修改返回值,因为没有命名
}()
return 42
}
想用 defer 修改返回值,必须用命名返回值。
defer 和 goroutine
func main() {
defer fmt.Println("main 结束")
go func() {
defer fmt.Println("goroutine 结束")
time.Sleep(time.Second)
}()
time.Sleep(500 * time.Millisecond)
// main 先结束,goroutine 可能来不及执行 defer
}
主函数的 defer 在主函数返回时执行,但 goroutine 的 defer 只在 goroutine 自己退出时执行。如果主函数先退出,goroutine 的 defer 可能不会执行。
学完这节你能做什么
- 用 defer 保证资源释放
- 理解 defer 的 LIFO 执行顺序
- 知道 defer 参数立即求值 vs 闭包延迟求值的区别
- 用 defer 修改命名返回值
- 了解 Go 1.14+ 的 defer 性能优化
下一节讲 panic 和 recover。