defer 延迟调用【GO 基础】
〇、前言
在 Go 语言中,defer 是一种用于延迟调用的关键字。
defer 在 Go 语言中的地位非常重要,它是确保资源正确释放和程序健壮性的关键字。
本文将通过示例对其进行专门的详解。
一、defer 简介
defer 的主要用途是在函数执行完毕之前,确保某个操作被执行。
通常用于:资源的释放管理,如关闭文件句柄、释放撕资源、网络连接、数据库连接等。
以下是 defer 的一些关键特性:
- 延迟执行:defer 后的函数调用不会立即执行,而是会推迟到包含它的函数执行结束时才执行
- 逆序执行:如果有多个 defer 语句,它们会按照后进先出的顺序执行,即最后一个被 defer 的语句会最先执行
- 作用域限制:defer 的作用域仅限于包围它的函数,不同于其他语言中的 finally 关键字,后者的作用域在其异常块中
- 语法简洁:在语法上,defer 与普通的函数调用没有区别,但是它提供了一种优雅的方式来确保资源的释放
- 资源管理:defer 常用于确保文件、网络连接等资源在使用完毕后被正确关闭,避免资源泄露。
在实际编程中,合理使用 defer 可以有效地提高代码的可读性和可维护性。下面会进行详细介绍。
二、defer 的使用
2.1 多 defer 时遵循先进后出 FILO
这个不难理解,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。
下面是一段示例代码,0~4 依次执行 defer,查看输出:
package main
import "fmt"
func main() {
fmt.Println("begin-------------------!")
var whatever [5]struct{}
for i := range whatever {
defer fmt.Println(i)
}
fmt.Println("end---------------------!")
}
输出结果是从 4 开始,说明先运行的 defer 后输出结果,且主线程先运行完成:

2.2 defer 与闭包
先解释一下闭包:它是一种编程概念,允许一个函数访问并操作其外部作用域中的变量。闭包通常出现在函数嵌套的情况下,即一个函数内部定义了另一个函数。内部的这个函数能够访问所在函数的变量和参数,无论外部函数已经执行完毕。这种结构使得内部函数保留对外部函数变量的引用,因此这些变量不会被垃圾回收机制收回。
闭包是由函数及其相关的引用环境组合而成的实体,可以理解为函数加上它所引用的外部变量。在Go语言中,所有的闭包都是匿名函数,但不是所有的匿名函数都是闭包。只有当匿名函数捕获了其外部作用域中的变量时,它才成为一个闭包。
官方对 defer 有句解释:
- Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.
- 每次执行“defer”语句时,函数值和调用的参数都会像往常一样求值并重新保存,但不会调用实际的函数。
如下一段示例代码,将上一章节的代码 defer 的内容换成一个函数:
package main
import "fmt"
func main() {
fmt.Println("begin-------------------!")
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Println(i) }()
}
fmt.Println("end---------------------!")
}
输出结果变成全部为 4,而非 4~0:

函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成 4,所以输出全都是 4。
下面将上边示例改良一下,在循环中添加新的变量暂存循环序列值 i:
package main
import "fmt"
func main() {
fmt.Println("begin-------------------!")
var whatever [5]struct{}
for i := range whatever {
ii := i // 看似多余的赋值,实际上是将公用变量赋值给局部变量
defer func() { fmt.Println(ii) }()
}
fmt.Println("end---------------------!")
}
运行结果:

2.3 某个 defer 发生异常,不会影响其他 defer 的正常执行
如下代码,在 b 和 c 之间的 defer 异常,但不影响 a 和 b 的正常执行:
package main
func test(x int) {
defer println("a")
defer println("b")
defer func() {
println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
}()
defer println("c")
}
func main() {
test(0)
}

2.4 延迟调用参数在注册时求值或复制,可用闭包 "延迟" 读取
如下代码,在 x 声明之后,将其作为参数传入 defer 函数体,此时 x 被赋值,后续的值变化将不会影响 defer 的输出:
package main
func test() {
x, y := 10, 20
defer func(i int) {
println("defer:", i, y) // y 闭包引用
}(x) // x 被复制
x += 10
y += 100
println("x =", x, "y =", y)
}
func main() {
test()
}

2.5 大循环时,用和不用 defer 的性能比较
如下代码,声明两个互斥锁,分别加锁和关锁,一个使用 defer 关锁,在循环次数较大时看下耗时情况:
package main
import (
"fmt"
"sync"
"time"
)
var lock sync.Mutex
var lock1 sync.Mutex
func test() {
lock.Lock()
lock.Unlock()
}
func testdefer() {
lock1.Lock()
defer lock1.Unlock()
}
func main() {
func() {
t1 := time.Now()
for i := 0; i < 1000000; i++ {
test()
}
elapsed := time.Since(t1)
fmt.Println("test elapsed: ", elapsed)
}()
func() {
t1 := time.Now()
for i := 0; i < 1000000; i++ {
testdefer()
}
elapsed := time.Since(t1)
fmt.Println("testdefer elapsed: ", elapsed)
}()
}
如下图测试结果,在一百万次时,起始差别也不太大:

三、defer 陷阱
3.1 执行闭包(Closure)时的特殊取值
一般情况下,defer 执行的代码行,是参数预加载的,也就是取当前行之前的值,之后变量值变化无影响。
但 defer 执行闭包就不一样了,会取之后代码执行完成后变量最终的值。
package main
import (
"errors"
"fmt"
)
func foo(a, b int) (i int, err error) {
defer fmt.Printf("first defer err %v\n", err)
// 普通的即时调用函数
defer func(err error) {
fmt.Printf("second defer err %v\n", err)
}(err)
// 此处为闭包,函数内部引用了外部的变量
defer func() {
fmt.Printf("third defer err %v\n", err)
}()
if b == 0 {
err = errors.New("divided by zero!")
return // 异常直接返回
}
i = a / b
return
}
func main() {
foo(2, 0)
}
// 执行结果,第三个 defer 能取到后续代码赋值给 err 的异常信息
third defer err divided by zero!
second defer err <nil>
first defer err <nil>
3.2 return 的使用
如下示例,函数 foo() 返回值是整数变量 i,第一次赋值是在 i=0 位置,在最后的 return 时,进行了第二次赋值,defer 执行的闭包是一直监视着变量 i 值的变化:
package main
import "fmt"
func foo() (i int) {
i = 0
defer func() {
fmt.Println("defer-i:", i)
}()
fmt.Println("return-before-i:", i)
return 2
}
func main() {
foo()
}
// 结果输出,由于 defer 是在全部代码执行完成后(包括 return)才运行
// 因此,defer 最后读取的是 return 赋值给 i 的值 2
return-before-i: 0
defer-i: 2
另外,defer 不可在有 return 分支的后边使用,这样可能造成 defer 为生效,达不到预期效果。这个很好理解,程序走到 return 后,就直接返回了,后边的代码就不再执行了。
3.3 延迟调用的函数为 nil 时引起 panic
如下示例代码,通过 recover() 函数捕捉 panic 信息:
注:recover() 函数的主要作用是在程序发生 panic 时,能够在 defer 语句中捕获到 panic 的信息,并返回导致 panic 的错误值。这样程序可以据此进行相应的处理,而不是直接崩溃退出。
package main
import (
"fmt"
)
func test() {
var run func() = nil
defer run()
fmt.Println("runs")
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
fmt.Println("end------------!")
}()
fmt.Println("start----------!")
test()
}
// 结果输出,最后的 end 记录可正常输出,说明程序没有异常退出
start----------!
runs
runtime error: invalid memory address or nil pointer dereference
end------------!
3.4 在 for 循环中调用 defer 并非立即执行
如下图中的示例代码,在 for 循环中每次加载 defer,但是并非每次都会执行,而是在 for 循环完成后一块执行:

这些延迟函数会不停地堆积到延迟调用栈中,最终可能会导致一些不可预知的问题。
解决方案有两种,如下:
1)直接在每次循环完成后,去掉 defer 关键字,直接执行 row.Close():
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/denisenkom/go-mssqldb" // go get github.com/denisenkom/go-mssqldb
)
func main() {
func() {
connStr := "server=<服务器地址>;user id=<用户名>;password=<密码>;database=<数据库名>;encrypt=disable"
// 连接到SQL Server
db, err := sql.Open("mssql", connStr)
if err != nil {
fmt.Println("db,err:", err)
return
}
for {
row, err := db.Query("select ID,shujubs from tablename where shujubs like '20240328%'")
if err != nil {
fmt.Println("row,err:", err)
}
row.Close()
}
}()
}
2)将 for 内循环体包含在新的函数中,在函数内使用 defer:
package main
import (
"database/sql"
"fmt"
"time"
_ "github.com/denisenkom/go-mssqldb" // go get github.com/denisenkom/go-mssqldb
)
func main() {
func() {
connStr := "server=<服务器地址>;user id=<用户名>;password=<密码>;database=<数据库名>;encrypt=disable"
// 连接到SQL Server
db, err := sql.Open("mssql", connStr)
if err != nil {
fmt.Println("db,err:", err)
return
}
for {
func() {
row, err := db.Query("select ID,shujubs from tablename where shujubs like '20240328%'")
if err != nil {
fmt.Println("row,err:", err)
}
defer row.Close() // 函数体中代码执行完后,就会执行 defer
}()
}
}()
}
3.5 在用 {} 包裹的代码块中使用 defer 是没有效果的
首先要知道的是,defer 延迟执行是相对于函数的,而非代码块。
如下代码来测试下,在独立代码块中使用 defer:
package main
import "fmt"
func main() {
fmt.Println("start----------!")
{ // 独立代码块
defer func() {
fmt.Println("block: defer runs")
}()
fmt.Println("block: ends")
}
fmt.Println("main: ends")
fmt.Println("end------------!")
}
如下输出结果,代码块中的 defer 内容,并没有在“main: ends”之前执行:

那么下边来优化下:
package main
import "fmt"
func main() {
fmt.Println("start----------!")
func() { // 将独立代码块,改造成函数
defer func() {
fmt.Println("block: defer runs")
}()
fmt.Println("block: ends")
}()
fmt.Println("main: ends")
fmt.Println("end------------!")
}

3.6 使用指针对象作为接收者和不用指针的对比
先看下不用指针的情况:
package main
import "fmt"
type Car struct {
model string
}
func (c Car) PrintModel() { // 不用指针对象
fmt.Println(c.model)
}
func main() {
fmt.Println("start----------!")
c := Car{model: "DeLorean DMC-12"}
defer c.PrintModel() // 此时就直接将对象 c 的值保存下来
c.model = "Chevrolet Impala" // 此处修改对上边已保存的 c 对象无效
fmt.Println("end------------!")
}

下面用指针对象试试看:
package main
import "fmt"
type Car struct {
model string
}
func (c *Car) PrintModel() { // 使用指针对象
fmt.Println(c.model)
}
func main() {
fmt.Println("start----------!")
c := Car{model: "DeLorean DMC-12"}
defer c.PrintModel() // 这里只保存了对象 c 的物理地址
c.model = "Chevrolet Impala" // 修改对象 c 后,defer 仍可取最新值
fmt.Println("end------------!")
}

参考:http://www.topgoer.com/%E5%87%BD%E6%95%B0/%E5%BB%B6%E8%BF%9F%E8%B0%83%E7%94%A8defer.html https://studygolang.com/articles/12061#commentForm
defer 延迟调用【GO 基础】的更多相关文章
- defer 延迟调用
1. 延迟调用 defer 的用法很简单,只要在后面跟一个函数的调用,就能实现将这个 xxx 函数的调用延迟到当前函数执行完后再执行. defer xxx() 这是一个很简单的例子,可以很快帮助 ...
- go语言基础之defer延迟调用
1.defer作用 关键字 defer ⽤于延迟一个函数或者方法(或者当前所创建的匿名函数)的执行.注意,defer语句只能出现在函数或方法的内部. 运行场景: defer语句经常被用于处理成对的操作 ...
- Swift Defer 延迟调用
1.Defer 在一些语言中,有 try/finally 这样的控制语句,比如 Java.这种语句可以让我们在 finally 代码块中执行必须要执行的代码,不管之前怎样的兴风作浪.在 Swift 2 ...
- Go语言系列开发之延迟调用和作用域
Hello,各位小伙伴大家好,我是小栈君,最近一段时间我们将继续分享关于go语言基础系列,当然后期小栈君已经在筹划关于java.Python,数据分析.人工智能和大数据等相关系列文章.希望能和大家一起 ...
- go 学习笔记之咬文嚼字带你弄清楚 defer 延迟函数
温故知新不忘延迟基础 A "defer" statement invokes a function whose execution is deferred to the momen ...
- ReactiveSwift源码解析(十一) Atomic的代码实现以及其中的Defer延迟、Posix互斥锁、递归锁
本篇博客我们来聊一下ReactiveSwift中的原子性操作,在此内容上我们简单的聊一下Posix互斥锁以及递归锁的概念以及使用场景.然后再聊一下Atomic的代码实现.Atomic主要负责多线程下的 ...
- Twisted 延迟调用
延迟(defer)是twisted框架中实现异步的编程体系,使程序设计可以采用事件驱动的机制 1.基本使用 defer可以看作一个管理回调函数的对象,可以向该对象添加需要的回调函数同时也可以指定该组函 ...
- 17_defer(延迟调用)关键字的使用
1.defer是延迟调用关键字,只能在函数内部使用 2.总是在main函数结束前调用(和init用法相对) 3.如果有多个defer 遵循先进后出的原则 4.和匿名函数同时使用时,如果匿名函数带有参数 ...
- go 学习笔记之解读什么是defer延迟函数
Go 语言中有个 defer 关键字,常用于实现延迟函数来保证关键代码的最终执行,常言道: "未雨绸缪方可有备无患". 延迟函数就是这么一种机制,无论程序是正常返回还是异常报错,只 ...
- 【Unity3D】Invoke,InvokeRepeating ,Coroutine 延迟调用,周期性调用
Invoke和InvokeRepeating方法,可以实现延迟调用,和周期调用 第一个是执行一次,第二个是重复执行 void Invoke(string methodName, float time) ...
随机推荐
- 【Android逆向】滚动的天空中插入smali日志
1. 编写一个MyLog.java 放到一个android工程下,编译打包,然后反编译拿到MyLog的smali代码 package com.example.logapplication; impor ...
- pikachu sql inject header 注入
使用admin登录 显示以下内容 朋友,你好,你的信息已经被记录了:点击退出 你的ip地址:172.17.0.1 你的user agent:Mozilla/5.0 (X11; Ubuntu; Linu ...
- Qt+MPlayer音乐播放器开发笔记(二):交叉编译MPlayer以及部署到开发板播放演示
前言 在ubuntu上arm交叉编译MPlayer播放器,并部署到开发板播放音乐. Demo Mplayer MPlayer是一款开源多媒体播放器,以GNU通 ...
- 看看这份2023年MySQL终级面试题,提升你的内力,给你面试助力
1.MySQL 中有哪几种锁? (1)表级锁:开销小,加锁快:不会出现死锁:锁定粒度大,发生锁冲突的概率最 高,并发度最低. (2)行级锁:开销大,加锁慢:会出现死锁:锁定粒度最小,发生锁冲突的概率最 ...
- Redis系列:RDB内存快照提供持久化能力
★ Redis24篇集合 1 介绍 从上一篇的 <深刻理解高性能Redis的本质> 中可以知道, 我们经常在数据库层上加一层缓存(如Redis),来保证数据的访问效率. 这样性能确实也有了 ...
- api网关介绍
1.什么是网关 API网关是一个系统的唯一入口. 是众多分布式服务唯一的一个出口. 它做到了物理隔离,内网服务只有通过网关才能暴露到外网被别人访问. 简而言之:网关就是你家的大门 2.提供了哪些功能 ...
- AT_abc342_d 题解
UD 2024/2/24 22:36 感谢 Lixiang_is_potato 指出一处笔误. 本文同步发表于洛谷. 赛时挂了,但是赛后 3min AC,我是飞舞. 题意 给你一个长度为 \(N\) ...
- 最强本地缓存Caffeine
Caffeine 是基于 JAVA 8 的高性能缓存库.并且在 spring5 (springboot 2.x) 后spring 官方放弃了 Guava,而使用了性能更优秀的 Caffeine 作为默 ...
- StatefulSet是怎样实现的
StatefulSet是Kubernetes中用于管理有状态应用的标准实现.与Deployment不同,StatefulSet为每个Pod提供了一个唯一的.稳定的网络标识符,并且Pod的启动和停止顺序 ...
- day12-面向对象03
面向对象03 10.抽象类 abstract修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么该方法就是抽象方法:如果修饰类,那么该类就是抽象类 抽象类中可以没有抽象方法,但是有抽象方法的类一定要 ...