时间因为回忆变得厚重
一、Go defer 白话核心:“先记下来,最后再执行”
defer 是 Go 里的一个关键字,作用是把紧跟它的函数调用 “登记” 到当前函数的 “延迟执行列表” 里,等当前函数执行完(return / 报错 / 正常结束)前,再按「后进先出」的顺序执行这些登记的函数。
大白话类比:你在收拾书桌(当前函数),过程中想到 “用完剪刀要归位”“喝完水杯要洗”“离开前要关灯”,就把这些事挨个记在便签上(defer 登记),等书桌收拾完(当前函数结束),再从最后记的事开始做(先关灯→洗水杯→归位剪刀)。
二、defer 基础用法(先看最简单的示例)
示例 1:基础执行顺序
package main
import "fmt"
func main() {
fmt.Println("开始执行main函数")
// 登记第一个defer函数
defer fmt.Println("执行defer 1:归位剪刀")
// 登记第二个defer函数
defer fmt.Println("执行defer 2:洗水杯")
// 登记第三个defer函数
defer fmt.Println("执行defer 3:关灯")
fmt.Println("正在收拾书桌...")
// main函数执行完,开始执行defer列表
}
输出结果:
开始执行main函数
正在收拾书桌...
执行defer 3:关灯
执行defer 2:洗水杯
执行defer 1:归位剪刀
核心规律:
- defer 函数「登记时不执行」,只记下来;
- 执行顺序是「后进先出(LIFO)」—— 最后登记的最先执行。
示例 2:defer 带参数(参数在登记时就确定值)
package main
import "fmt"
func main() {
num := 10
// 登记时,参数num的值已经确定是10(不是执行时的20)
defer fmt.Println("defer执行:num =", num)
num = 20
fmt.Println("main执行:num =", num)
}
输出结果:
main执行:num = 20
defer执行:num = 10
关键坑点:defer 函数的参数在「登记时」就计算出值并保存,执行时不会再读最新值。
示例 3:defer 修饰函数(不是直接写语句)
package main
import "fmt"
// 定义一个普通函数
func sayHi(name string) {
fmt.Printf("Hi, %s\n", name)
}
func main() {
// defer 可以修饰函数调用(参数同样在登记时确定)
defer sayHi("张三")
defer sayHi("李四")
fmt.Println("main函数执行中")
}
输出结果:
main函数执行中
Hi, 李四
Hi, 张三
三、defer 核心特性(白话拆解)
1. 执行时机:当前函数 “退出前”(3 种场景)
- 场景 1:函数正常执行完 return 后;
- 场景 2:函数发生 panic(崩溃)时;
- 场景 3:函数被手动调用 runtime.Goexit() 退出时。
示例:panic 时 defer 仍执行
package main
import "fmt"
func main() {
defer fmt.Println("defer:程序崩溃了也会执行我")
fmt.Println("main执行中...")
// 手动触发panic
panic("发生错误!")
fmt.Println("这行不会执行") // panic后函数直接退出,这行跳过
}
输出结果:
main执行中...
defer:程序崩溃了也会执行我
panic: 发生错误!
...(后续panic堆栈信息)
2. defer 可以修改函数返回值(关键!)
如果 defer 函数是「闭包」(引用外部变量),能修改函数的返回值(因为返回值在 defer 执行前已经分配内存)。
示例 1:修改命名返回值
package main
import "fmt"
// 命名返回值:res 是提前定义的返回变量
func calc() (res int) {
defer func() {
res += 10 // defer 闭包修改返回值res
}()
res = 5 // 先赋值5
return // 返回时,先执行defer,再返回res
}
func main() {
fmt.Println(calc()) // 输出 15(5+10)
}
示例 2:不修改匿名返回值(对比)
package main
import "fmt"
// 匿名返回值:返回值没有提前命名
func calc() int {
num := 5
defer func() {
num += 10 // 修改的是局部变量num,不是返回值
}()
return num // 返回值在return时已经确定是5,defer改num没用
}
func main() {
fmt.Println(calc()) // 输出 5
}
核心结论:只有「命名返回值」能被 defer 闭包修改,匿名返回值不行。
3. defer 释放资源(最常用的业务场景)
这是 defer 最核心的实际用途:打开资源(文件 / 连接 / 锁)后,立刻用 defer 登记 “关闭资源”,避免忘记释放。
示例 1:关闭文件
package main
import (
"fmt"
"os"
)
func readFile(filename string) {
// 打开文件
file, err := os.Open(filename)
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
// 登记关闭文件(不管函数怎么退出,都会执行)
defer func() {
err := file.Close()
if err != nil {
fmt.Println("关闭文件失败:", err)
} else {
fmt.Println("文件已关闭")
}
}()
// 读取文件(模拟业务逻辑)
fmt.Println("正在读取文件内容...")
}
func main() {
readFile("test.txt")
}
示例 2:释放锁
package main
import (
"fmt"
"sync"
)
var mutex sync.Mutex
var count int
func add() {
// 加锁
mutex.Lock()
// 登记解锁(避免忘记解锁导致死锁)
defer mutex.Unlock()
count++
fmt.Println("count =", count)
}
func main() {
add()
add()
}
示例 3:关闭数据库连接
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func queryDB() {
// 连接数据库
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/test")
if err != nil {
fmt.Println("连接数据库失败:", err)
return
}
// 登记关闭连接
defer db.Close()
// 执行查询(模拟业务)
fmt.Println("执行数据库查询...")
}
func main() {
queryDB()
}
四、defer 常见坑点(避坑示例)
坑点 1:循环中使用 defer 导致资源泄漏
package main
import (
"fmt"
"os"
)
// 错误写法:循环里的defer会等到函数结束才执行,导致同时打开100个文件
func badExample() {
for i := 0; i < 100; i++ {
file, err := os.Open(fmt.Sprintf("file%d.txt", i))
if err != nil {
continue
}
defer file.Close() // 登记100个close,但都等函数结束才执行
// 读取文件...
}
}
// 正确写法:把循环体逻辑抽成子函数,defer在子函数里执行
func goodExample() {
for i := 0; i < 100; i++ {
// 子函数执行完就会关闭文件
func(num int) {
file, err := os.Open(fmt.Sprintf("file%d.txt", num))
if err != nil {
return
}
defer file.Close()
// 读取文件...
}(i)
}
}
func main() {
goodExample()
}
坑点 2:defer 执行顺序与 return 混用
package main
import "fmt"
func foo() int {
defer fmt.Println("defer执行")
return 10
}
func main() {
res := foo()
fmt.Println("main拿到结果:", res)
}
输出结果:
defer执行
main拿到结果: 10
执行流程:return 10 → 先把返回值 10 赋值给临时变量 → 执行 defer → 把临时变量返回给 main。
五、defer 典型业务场景总结
- 资源释放:文件 / 数据库连接 / 网络连接 / 锁的关闭 / 释放(最核心场景);
- 异常恢复:配合 recover() 捕获 panic,避免程序崩溃;
- 日志记录:函数退出前记录执行时间、结果等日志;
- 清理临时数据:函数执行完删除临时文件、清空缓存等。
示例:defer + recover 捕获 panic
package main
import "fmt"
func riskyFunc() {
// 登记recover,捕获panic
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获到错误:", err)
}
}()
// 触发panic
panic("数组越界")
}
func main() {
riskyFunc()
fmt.Println("程序继续执行,没有崩溃")
}
输出结果:
捕获到错误: 数组越界
程序继续执行,没有崩溃
总结
- defer 核心:登记函数延迟执行,执行时机是当前函数退出前,顺序是「后进先出」;
- 关键特性:参数在登记时确定值,可修改命名返回值,panic 时仍会执行;
- 核心用途:释放资源(文件 / 连接 / 锁)、捕获 panic、日志记录,是 Go 工程化开发的必备技巧;
- 避坑要点:循环中避免直接用 defer,区分命名 / 匿名返回值的修改规则。
简单记:defer 就是 “先记后做,倒序执行,兜底释放”,只要涉及 “打开 - 关闭” 类操作,第一时间加 defer 准没错。