go 关键字 defer 使用详解(使用场景易错分析)
目录
go 关键字 defer 使用详解(使用场景易错分析)
核心原理
defer 是 Go 语言的一大特性,defer的字面意思是“延迟”,但很多人只知道它“延迟执行”,却不懂背后的逻辑,这是踩坑的根源。
先记住3个核心原理:
- 执行时机:defer修饰的函数会在“所在函数执行完return语句后、真正返回前”执行,不是在函数结束时才执行,也不是在defer定义时执行;
- 参数预计算:defer修饰的函数的参数,在定义defer时就会计算出具体值,而不是在执行defer时才计算;
- 栈式执行:多个defer同时存在时,遵循“后进先出”(LIFO)的栈式顺序,最后定义的defer最先执行。
用 defer 可以来声明一些延迟函数,这些延迟函数会被放进队列里,在外层函数执行完成后会依次执行延迟函数:
- 在函数、方法返回 return 之前执行
- 可以同时设置多个 defer 函数,执行顺序遵循 FILO 先进后出 顺序
- defer 会将运行结果存在栈中,等到 return 之后再推出来执行
- 一般常用于资源清理、锁释放、文件资源关闭、关闭数据库连接、与 panic recover 结合使用等
场景一、基础使用
简单使用
func main() {
defer fmt.Println(1) // 输出1
x = 2
fmt.Println(x)
return
}2
1
场景二:先进后出
多个 defer 的执行顺序
func main() {
defer fmt.Println(1) // 输出1
defer fmt.Println(2) // 输出1
x = 3
fmt.Println(x)
return
}3
2
1
场景三:闭包使用
在闭包调用下 defer 的执行情况,包含参数的引用结果
func main() {
fmt.Println("res:", testDefer()) // res:3
}
func testDefer() int {
var i = 1
defer func() { // 将函数存储在栈上,最后执行的时候调用该函数,打印出i,所以i的值在后面的改动生效
fmt.Println("defer2:", i) // i=3
}()
i++ // i=2
defer fmt.Println("defer3:", i) // 将结果“2” 存储在栈上,执行时输出
defer fmt.Println("defer4:", i+1) // 结果 3,但是不会影响 i 的值
i++ //i=3
return i // 可以看作 var rsp=i,return rsp,所以defer里改了i的值时没用的
}结果: defer4: 3 defer3: 2 defer2: 3 res: 3
四、生产场景
defer 在生成环境中有最常见的需求:打开文件、建立数据库连接、创建锁后,必须确保关闭/释放,否则会导致资源泄漏。defer能保证“无论函数正常执行还是因错误返回,都会释放资源”,比手动在每个return前写释放代码高效且不易漏。
- 打印耗时
func ProcessJob() {
start := time.Now()
defer func() {
fmt.Printf("耗时: %v", time.Since(start))
}()
// 复杂业务逻辑...
}- 资源关闭
func ProcessFileFixed() {
f, _ := os.Open("data.log")
defer func(){
_=f.Close() // 确保资源释放
}
// 业务逻辑...
}- 锁关闭
func ProcessJob() {
var mu=sync.Mutex
mu.Lock()
defer func(){
mu.Unlock() // 解锁,避免死锁
}
// 业务逻辑...
}- 事务提交
func TransferMoney(db *sql.DB) error {
var err error
tx, err := db.Begin()
if err!=nil{
return err
}
defer func() {
if err!=nil{
tx.Rollback()
}else{
tx.Commit()
}
}()
// 业务逻辑...
return nil
}- panic 捕获
func ProcessJob() {
start := time.Now()
defer func() {
if p := recover(); p != nil {
// 打印堆栈日志
}
}()
// 复杂业务逻辑...触发 panic
return
}总结
defer的核心价值是“保证延迟执行的可靠性”,掌握以下4个法则,就能避开绝大部分的坑:
- 资源释放必用defer:打开文件、连接、锁后,立刻defer释放,且要处理释放错误;
- 参数求值看定义:defer的参数在定义时就确定,要动态计算值就用匿名函数包裹;
- 执行顺序栈先进:多个defer按“后进先出”执行,定义顺序要和资源依赖相反;
- 循环defer要包裹:循环中使用defer时,用匿名函数包裹,避免资源累积。
defer看似简单,但用好它能极大提升代码的健壮性,这也是Go语言“简洁而强大”的体现。
如果大家有defer的特殊使用场景或踩坑经历,欢迎在评论区交流!
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!