什么是defer?如何理解 defer 关键字?Go 中使用 defer 的一些坑。
defer 意为延迟,在 golang 中用于延迟执行一个函数。它可以帮助我们处理容易忽略的问题,如资源释放、连接关闭等。但在实际使用过程中,有一些需要注意的地方.
1. 若函数中有多个 defer,其执行顺序为 先进后出,可以理解为栈。
~~~go
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
}
~~~
运行:
~~~go
4
3
2
1
0
~~~
2. return 会做什么呢?
Go 的函数返回值是通过堆栈返回的, return 语句不是原子操作,而是被拆成了两步.
* 给返回值赋值 (rval)
* 调用 defer 表达式
* 返回给调用函数(ret)
~~~go
package main
import "fmt"
func main() {
fmt.Println(increase(1))
}
func increase(d int) (ret int) {
defer func() {
ret++
}()
return d
}
~~~
运行输出:
~~~go
2
~~~
3. 若 defer 表达式有返回值,将会被丢弃。
闭包与匿名函数.
* 匿名函数:没有函数名的函数。
* 闭包:可以使用另外一个函数作用域中的变量的函数。
在实际开发中,defer 的使用经常伴随着闭包与匿名函数的使用。
~~~go
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
defer func() {
fmt.Println(i)
}()
}
}
~~~
运行输出:
~~~go
5
5
5
5
5
~~~
之所以这样是因为,defer 表达式中的 i 是对 for 循环中 i 的引用。到最后,i 加到 5,故最后全部打印 5。
如果将 i 作为参数传入 defer 表达式中,在传入最初就会进行求值保存,只是没有执行延迟函数而已。
应用示例:
~~~go
func f1() (result int) {
defer func() {
result++
}()
return 0
}
~~~
~~~go
func f2() (r int) {
t := 5
defer func() {
t = t + 5
}()
return t
}
~~~
~~~go
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
~~~
~~~go
type Test struct {
Max int
}
func (t *Test) Println() {
fmt.Println(t.Max)
}
func deferExec(f func()) {
f()
}
func call() {
var t *Test
defer deferExec(t.Println)
t = new(Test)
}
~~~
有没有得出结果?例1的答案不是 0,例2的答案不是 10,例3的答案也不是 6。
defer是在return之前执行的。这个在[官方文档](https://golang.org/ref/spec#defer_statements)中是明确说明了的。要使用defer时不踩坑,最重要的一点就是要明白,`return xxx`这一条语句并不是一条原子指令!
~~~gfm
函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。
defer表达式可能会在设置函数返回值之后,在返回到调用函数之前,修改返回值,使最终的函数返回值与你想象的不一致。
其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:
返回值 = xxx
调用defer函数
空的return
~~~
f1: 比较简单,参考结论2,将 0 赋给 result,defer 延迟函数修改 result,最后返回给调用函数。正确答案是 1。
f1可以修改成长这样的:
~~~go
func f() (result int) {
result = 0 // return语句不是一条原子调用,return xxx其实是赋值+ret指令
func() { // defer被插入到return之前执行,也就是赋返回值和ret指令之间
result++
}()
return
}
~~~
所以这个返回值是1。
f2: defer 是在 t 赋值给 r 之后执行的,而 defer 延迟函数只改变了 t 的值,r 不变。正确答案 5。
f2可以修改成这样的:
~~~go
func f() (r int) {
t := 5
r = t // 赋值指令
func() { // defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
t = t + 5
}
return // 空的return指令
}
~~~
所以这个的结果是5。
f3: 这里将 r 作为参数传入了 defer 表达式。故 func (r int) 中的 r 非 func f() (r int) 中的 r,只是参数命名相同而已。正确答案 1。
f3可以修改成这样的:
~~~go
func f() (r int) {
r = 1 // 给返回值赋值
func(r int) { // 这里改的r是传值传进去的r,不会改变要返回的那个r值
r = r + 5
}(r)
return // 空的return
}
~~~
所以这个例子的结果是1。
f4: 这里将发生 panic。将方法传给 deferExec,实际上在传的过程中对方法求了值。而此时的 t 任然为 nil。
因此, defer确实是在return之前调用的。但表现形式上却可能不像。根本原因是`return xxx`语句并不是一条原子指令,defer被插入到了赋值 与 ret之间,因此可能有机会改变最终的返回值。
defer关键字的实现跟go关键字很类似,不同的是它调用的是`runtime.deferproc`而不是`runtime.newproc`。
在defer出现的地方,插入了指令`call runtime.deferproc`,然后在函数返回之前的地方,插入指令`call runtime.deferreturn`。
普通的函数返回时,汇编代码类似:
~~~go
add xx SP
return
~~~
如果其中包含了defer语句,则汇编代码是:
~~~go
call runtime.deferreturn,
add xx SP
return
~~~
goroutine的控制结构中,有一张表记录defer,调用`runtime.deferproc`时会将需要defer的表达式记录在表中,而在调用`runtime.deferreturn`的时候,则会依次从defer表中出栈并执行。
- Golang基础
- Go中new与make的区别
- Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量
- 无缓冲Chan的发送和接收是否同步
- Golang并发机制以及它所使用的CSP并发模型.
- Golang中常用的并发模型
- Go中对nil的Slice和空Slice的处理是一致的吗
- 协程和线程和进程的区别
- Golang的内存模型中为什么小对象多了会造成GC压力
- Go中数据竞争问题怎么解决
- 什么是channel,为什么它可以做到线程安全
- Golang垃圾回收算法
- GC的触发条件
- Go的GPM如何调度
- 并发编程概念是什么
- Go语言的栈空间管理是怎么样的
- Goroutine和Channel的作用分别是什么
- 怎么查看Goroutine的数量
- Go中的锁有哪些
- 怎么限制Goroutine的数量
- Channel是同步的还是异步的
- Goroutine和线程的区别
- Go的Struct能不能比较
- Go的defer原理是什么
- Go的select可以用于什么
- Context包的用途是什么
- Go主协程如何等其余协程完再操作
- Go的Slice如何扩容
- Go中的map如何实现顺序读取
- Go中CAS是怎么回事
- Go中的逃逸分析是什么
- Go值接收者和指针接收者的区别
- Go的对象在内存中是怎样分配的
- 栈的内存是怎么分配的
- 堆内存管理怎么分配的
- 在Go函数中为什么会发生内存泄露
- G0的作用
- Go中的锁如何实现
- Go中的channel的实现
- 栈的内存是怎么分配的2
- 堆内存管理怎么分配的2
- Go中的map的实现
- Go中的http包的实现原理
- Goroutine发生了泄漏如何检测
- Go函数返回局部变量的指针是否安全
- Go中两个Nil可能不相等吗
- Goroutine和KernelThread之间是什么关系
- 为何GPM调度要有P
- 如何在goroutine执行一半就退出协程
- Mysql基础
- Mysql索引用的是什么算法
- Mysql事务的基本要素
- Mysql的存储引擎
- Mysql事务隔离级别
- Mysql高可用方案有哪些
- Mysql中utf8和utf8mb4区别
- Mysql中乐观锁和悲观锁区别
- Mysql索引主要是哪些
- Mysql联合索引最左匹配原则
- 聚簇索引和非聚簇索引区别
- 如何查询一个字段是否命中了索引
- Mysql中查询数据什么情况下不会命中索引
- Mysql中的MVCC是什么
- Mvcc和Redolog和Undolog以及Binlog有什么不同
- Mysql读写分离以及主从同步
- InnoDB的关键特性
- Mysql如何保证一致性和持久性
- 为什么选择B+树作为索引结构
- InnoDB的行锁模式
- 哈希(hash)比树(tree)更快,索引结构为什么要设计成树型
- 为什么索引的key长度不能太长
- Mysql的数据如何恢复到任意时间点
- Mysql为什么加了索引可以加快查询
- Explain命令有什么用
- Redis基础
- Redis的数据结构及使用场景
- Redis持久化的几种方式
- Redis的LRU具体实现
- 单线程的Redis为什么快
- Redis的数据过期策略
- 如何解决Redis缓存雪崩问题
- 如何解决Redis缓存穿透问题
- Redis并发竞争key如何解决
- Redis的主从模式和哨兵模式和集群模式区别
- Redis有序集合zset底层怎么实现的
- 跳表的查询过程是怎么样的,查询和插入的时间复杂度
- 网络协议基础
- TCP和UDP有什么区别
- TCP中三次握手和四次挥手
- TCP的LISTEN状态是什么
- 常见的HTTP状态码有哪些
- 301和302有什么区别
- 504和500有什么区别
- HTTPS和HTTP有什么区别
- Quic有什么优点相比Http2
- Grpc的优缺点
- Get和Post区别
- Unicode和ASCII以及Utf8的区别
- Cookie与Session异同
- Client如何实现长连接
- Http1和Http2和Grpc之间的区别是什么
- Tcp中的拆包和粘包是怎么回事
- TFO的原理是什么
- TIME_WAIT的作用
- 网络的性能指标有哪些