深入理解golang的defer
defer 估计是每个 Gopher 每天写代码都会写,那么你是不是真正的理解了 defer 呢?不妨看一下下面这个代码片段,这个是我之前给 UC 那边一个 team 做 Golang 培训的时候想的例子。
package mainfunc f() int {i := 5defer func() {i++}()return i
}func f1() (result int) { defer func() { result++ }() return 0
}func f2() (r int) { t := 5 defer func() { t = t + 5 }()return t
}func f3() (r int) { defer func(r int) { r = r + 5 }(r) return 1
}func main() {println(f())println(f1())println(f2())println(f3())
}
1. return 语句
在解析上面的题目之前,要理解一个前提是 Go 的函数返回值是通过堆栈返回的,这也是实现了多返回值的方法。举个例子。
//foo.go
package mainfunc foo() (int,int){i := 1j := 2return i,j
}func main() {foo()
}
查看汇编代码如下。
$ go build -gcflags '-l' -o foo foo.go
$ go tool objdump -s "main\.foo" foo
TEXT main.foo(SB) /Users/kltao/code/go/src/example/foo.gobar.go:6 0x104ea70 48c744240801000000 MOVQ $0x1, 0x8(SP)bar.go:6 0x104ea79 48c744241002000000 MOVQ $0x2, 0x10(SP)bar.go:6 0x104ea82 c3 RET
也就是说 return 语句不是原子操作,而是被拆成了两步
rval = xxx
ret
而 defer 语句就是在这两条语句之间执行,也就是
rval = xxx
defer_func
ret
另外在 Go 语言的 func 声明中如果返回值变量显示声明,也就是 func foo() (ret int) {}
的时候,rval 就是 ret。这么上面的题目中对于的函数执行简单来说就是如下代码片段。但是 f3 涉及到另外一个知识点,也就是闭包。
//f
rval = i
i ++
ret//f1
result = 0
defer // result ++
return//f2
r = t
defer // t = t + 5
return
2. 闭包
简单来说,Go 语言中的闭包就是在函数内引用函数体之外的数据,这样就会产生一种结果,虽然数据定义是在函数外,但是在函数内部操作数据也会对数据产生影响。如下面的例子所示,foo() 中的匿名函数对 i 的调用就是闭包引用,i++ 会影响外面定义的 i 的值。而 bar() 中的匿名函数是变量拷贝,i++ 并不会修改外部 i 值。这么看的话,开始的 f3() 的输出你是不是知道是多少了呢?
func foo() {i := 1go func() {i ++ }()time.Sleep(xxx)println(i)
}func bar() {i := 1go func(i int) {i ++}(i)time.Sleep(xxx)println(i)
}
3. defer 的使用场景
在我最开始学习 Go 语言的时候,我看到 defer 的第一反应就是 Python 中的如下语句。也就是说不用显示地关闭文件句柄,除此之外还有网络连接等各种资源都可以放到 defer 里面来释放。
with open("file", "a") as f:// handler
但是随着写代码越来越多,我觉得上面说的这些场景如果明确知道什么时候要释放资源,那么都不是非使用 defer 不可的,因为使用 defer 还是有很大开销的,下面说。使用 defer 的最合适的场景我觉得应该是和 recover 结合使用,也就是说在你不知道的程序何时可能会 panic 的时候,才引入 defer + recover。
func f() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered in f", r)}}()
}
4. defer 的底层实现
defer 的底层实现主要由两个函数:
- func deferproc(siz int32, fn *funcval)
- func deferreturn(arg0 uintptr)
看代码。下面的代码执行了两次 defer ,defer 的执行是按 FILO 的次序执行的,也就是说下面代码的输出是
world
hello2
hello1
这个就不细说了。看汇编代码。
package mainimport ("fmt"
)func main() {defer fmt.Println("hello1")defer fmt.Println("hello2")fmt.Println("world")
}
编译,objdump。
$ go build -gcflags '-l' -o defer defer.go
$ go tool objdump -s "main\.main" defer
TEXT main.main(SB) /Users/kltao/code/go/src/example/defer2.go...defer2.go:8 0x1092fe1 0f57c0 XORPS X0, X0defer2.go:8 0x1092fe4 0f11442450 MOVUPS X0, 0x50(SP)defer2.go:8 0x1092fe9 488d05100c0100 LEAQ type.*+68224(SB), AXdefer2.go:8 0x1092ff0 4889442450 MOVQ AX, 0x50(SP)defer2.go:8 0x1092ff5 488d0db4b00400 LEAQ main.statictmp_0(SB), CXdefer2.go:8 0x1092ffc 48894c2458 MOVQ CX, 0x58(SP)defer2.go:8 0x1093001 c7042430000000 MOVL $0x30, 0(SP)defer2.go:8 0x1093008 488d0d999d0300 LEAQ go.func.*+8(SB), CXdefer2.go:8 0x109300f 48894c2408 MOVQ CX, 0x8(SP)defer2.go:8 0x1093014 488d542450 LEAQ 0x50(SP), DXdefer2.go:8 0x1093019 4889542410 MOVQ DX, 0x10(SP)defer2.go:8 0x109301e 48c744241801000000 MOVQ $0x1, 0x18(SP)defer2.go:8 0x1093027 48c744242001000000 MOVQ $0x1, 0x20(SP)defer2.go:8 0x1093030 e81b3bf9ff CALL runtime.deferproc(SB)defer2.go:8 0x1093035 85c0 TESTL AX, AXdefer2.go:8 0x1093037 0f85b8000000 JNE 0x10930f5defer2.go:9 0x109303d 0f57c0 XORPS X0, X0defer2.go:9 0x1093040 0f11442440 MOVUPS X0, 0x40(SP)defer2.go:9 0x1093045 488d05b40b0100 LEAQ type.*+68224(SB), AXdefer2.go:9 0x109304c 4889442440 MOVQ AX, 0x40(SP)defer2.go:9 0x1093051 488d0d68b00400 LEAQ main.statictmp_1(SB), CXdefer2.go:9 0x1093058 48894c2448 MOVQ CX, 0x48(SP)defer2.go:9 0x109305d c7042430000000 MOVL $0x30, 0(SP)defer2.go:9 0x1093064 488d0d3d9d0300 LEAQ go.func.*+8(SB), CXdefer2.go:9 0x109306b 48894c2408 MOVQ CX, 0x8(SP)defer2.go:9 0x1093070 488d4c2440 LEAQ 0x40(SP), CXdefer2.go:9 0x1093075 48894c2410 MOVQ CX, 0x10(SP)defer2.go:9 0x109307a 48c744241801000000 MOVQ $0x1, 0x18(SP)defer2.go:9 0x1093083 48c744242001000000 MOVQ $0x1, 0x20(SP)defer2.go:9 0x109308c e8bf3af9ff CALL runtime.deferproc(SB)defer2.go:9 0x1093091 85c0 TESTL AX, AXdefer2.go:9 0x1093093 7550 JNE 0x10930e5defer2.go:11 0x1093095 0f57c0 XORPS X0, X0defer2.go:11 0x1093098 0f11442460 MOVUPS X0, 0x60(SP)defer2.go:11 0x109309d 488d055c0b0100 LEAQ type.*+68224(SB), AXdefer2.go:11 0x10930a4 4889442460 MOVQ AX, 0x60(SP)defer2.go:11 0x10930a9 488d0520b00400 LEAQ main.statictmp_2(SB), AXdefer2.go:11 0x10930b0 4889442468 MOVQ AX, 0x68(SP)defer2.go:11 0x10930b5 488d442460 LEAQ 0x60(SP), AXdefer2.go:11 0x10930ba 48890424 MOVQ AX, 0(SP)defer2.go:11 0x10930be 48c744240801000000 MOVQ $0x1, 0x8(SP)defer2.go:11 0x10930c7 48c744241001000000 MOVQ $0x1, 0x10(SP)defer2.go:11 0x10930d0 e80b99ffff CALL fmt.Println(SB)defer2.go:12 0x10930d5 90 NOPLdefer2.go:12 0x10930d6 e80543f9ff CALL runtime.deferreturn(SB)defer2.go:12 0x10930db 488b6c2470 MOVQ 0x70(SP), BPdefer2.go:12 0x10930e0 4883c478 ADDQ $0x78, SPdefer2.go:12 0x10930e4 c3 RETdefer2.go:9 0x10930e5 90 NOPLdefer2.go:9 0x10930e6 e8f542f9ff CALL runtime.deferreturn(SB)defer2.go:9 0x10930eb 488b6c2470 MOVQ 0x70(SP), BPdefer2.go:9 0x10930f0 4883c478 ADDQ $0x78, SPdefer2.go:9 0x10930f4 c3 RET...
结合代码看,代码中使用了两次 defer,调用了 deferproc 和 deferreturn ,都是匹配成对调用的。我们看一下 Golang 源码里面对 deferproc 和 deferreturn 的实现。
// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fnif getg().m.curg != getg() { // getg 是获取当前的 goroutine// go code on the system stack can't deferthrow("defer on system stack")}// the arguments of fn are in a perilous state. The stack map// for deferproc does not describe them. So we can't let garbage// collection or stack copying trigger until we've copied them out// to somewhere safe. The memmove below does that.// Until the copy completes, we can only call nosplit routines.sp := getcallersp()argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)callerpc := getcallerpc()d := newdefer(siz) // 申请一个结构体用来存放 defer 相关数据if d._panic != nil {throw("deferproc: d.panic != nil after newdefer")}d.fn = fnd.pc = callerpcd.sp = spswitch siz {case 0:// Do nothing.case sys.PtrSize:*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))default:memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))}// deferproc returns 0 normally.// a deferred func that stops a panic// makes the deferproc return 1.// the code the compiler generates always// checks the return value and jumps to the// end of the function if deferproc returns != 0.return0()// No code can go here - the C return register has// been set and must not be clobbered.
}
光看 deferproc 的代码只能看到一个申请 defer 对象的过程,并没有看到这个 defer 对象存储在哪里?那么不妨大胆设想一下,defer 对象是以链表的形式关联到 goroutine 上的。我们看一下 deferproc 中调用的 newdefer 函数。
func newdefer(siz int32) *_defer {var d *_defersc := deferclass(uintptr(siz))gp := getg()if sc < uintptr(len(p{}.deferpool)) {pp := gp.m.p.ptr()if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {// Take the slow path on the system stack so// we don't grow newdefer's stack.systemstack(func() {lock(&sched.deferlock)for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {d := sched.deferpool[sc]sched.deferpool[sc] = d.linkd.link = nilpp.deferpool[sc] = append(pp.deferpool[sc], d)}unlock(&sched.deferlock)})}if n := len(pp.deferpool[sc]); n > 0 {d = pp.deferpool[sc][n-1]pp.deferpool[sc][n-1] = nilpp.deferpool[sc] = pp.deferpool[sc][:n-1]}}if d == nil {// Allocate new defer+args.systemstack(func() {total := roundupsize(totaldefersize(uintptr(siz)))d = (*_defer)(mallocgc(total, deferType, true))})if debugCachedWork {// Duplicate the tail below so if there's a// crash in checkPut we can tell if d was just// allocated or came from the pool.d.siz = sizd.link = gp._defergp._defer = dreturn d}}d.siz = sizd.link = gp._defergp._defer = dreturn d
}
重点看第 44,45 行,gp 是当前的 goroutine,有一个字段 _defer 是用来存放 defer 结构的,然后我们发现 defer 结构有一个 link 字段其实就相当于链表指针。如果熟悉链表操作的话,第 44,45 行结合起来看就是将新的 defer 对象插入到 goroutine 关联的 defer 链表的头部。那么执行的时候就从头执行 defer 就是 FILO 的顺序了,deferreturn 的源码大家自己去看吧。
5. benchmark
看了第 4 部分,我们应该知道 defer 的调用开销相比直接的函数调用确实多了不少,那么有没有 benchmark 来直观的看一下呢?有的。这里使用雨痕的 《Go 语言学习笔记》的 benchmark 程序。
package mainimport ("testing""sync"
)var m sync.Mutexfunc call() {m.Lock()m.Unlock()
}func deferCall() {m.Lock()defer m.Unlock()
}func BenchmarkCall(b *testing.B) {for i:=0; i<b.N; i++ {call()}
}func BenchmarkDeferCall(b *testing.B) {for i:=0; i<b.N; i++ {deferCall()}
}
测试结果如下,看的出来差距还是挺大的。
➜ df go test -bench=.
goos: darwin
goarch: amd64
pkg: example/df
BenchmarkCall-8 100000000 17.8 ns/op
BenchmarkDeferCall-8 20000000 56.3 ns/op
6. 参考
- https://github.com/golang/go/blob/release-branch.go1.12/src/runtime/panic.go#L229
- https://github.com/golang/go/blob/master/src/runtime/asm_amd64.s#L550
- 《Go 语言学习笔记》
最后,我之前只在博客 http://www.legendtkl.com 和知乎上(知乎专栏:Golang Inside)上面写文章,现在开始在公众号(公众号:legendtkl)上面尝试一下,如果你觉得不错,或者之前看过,欢迎关注或者推荐给身边的人。谢谢。
深入理解golang的defer相关推荐
- 理解Golang中defer的使用
之前一直对Go中的defer不太理解,所以我单独弄出来整理一下. 在golang当中,defer代码块会在函数调用链表中增加一个函数调用.这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是r ...
- golang的defer的理解- defer的函数一定会执行吗?
文章目录 golang的defer 什么是defer 理解defer defer什么时间执行(defer. return.返回值 三者的执行顺序) defer输出的值,就是定义时的值.而不是defer ...
- 深入理解Golang之context
深入理解Golang之context context是Go并发编程中常用到一种编程模式.本文将从为什么需要context,深入了解context的实现原理,以了解如何使用context. 作者:Tur ...
- golang的defer机制详解
defer概述 defer用来声明一个延迟函数,把这个函数放入到一个栈上,当外部的包含方法return之前,返回参数到调用方法之前调用,也可以说是运行到最外层方法体时调用.我们经常用他来做一些资源的释 ...
- 深入理解Golang 编程思维和工程实战
| 导语 Golang 的一些编程思维和思想,以及总结一些常见的优雅编程实战技巧 目录 一 Golang 编程思维 二 Golang 高级编码技巧 1 优雅的实现构造函数编程思想 2 优雅的实现继承编 ...
- 深入理解Golang中的Context包
context.Context是Go语言中独特的设计,在其他编程语言中我们很少见到类似的概念.context.Context深度支持Golang的高并发. 1. Goroutine和Channel 在 ...
- 理解Golang的Time结构
在golang中创建并打印一个时间对象,会看到如下输出 2018-10-26 14:15:50.306558969 +0800 CST m=+0.000401093 复制代码 前面表示的意义好理解,分 ...
- 深入理解Golang包导入
Golang使用包(package)这种语法元素来组织源码,所有语法可见性均定义在package这个级别,与Java .python等语言相比,这算不上什么创新,但与C传统的include相比,则是显 ...
- Go实战--golang中defer的使用
原址 生命不止,继续 go go go !!! 学习golang这么久了,还没看到类似传统的 try-catch-finally 这种异常捕捉方式. 但是,Go中引入的Exception处理:def ...
最新文章
- Android NDK基础样例
- Lync Server 2010的部署系列_第三章 证书、架构、DNS规划
- 使用Docker-数据卷命令
- java 支付宝 退款_Java 支付宝支付,退款,单笔转账到支付宝账户(支付宝支付)
- 小猫的java基础知识点汇总(上)
- 2019快手内容报告重磅发布:日活突破3亿 点赞量超3500亿
- Homebrew命令具体解释
- ActiveMQ 权限
- 暴力推荐2:硬盘分区丢失之DiskGenius
- 通过Cookie存取用户游览记录的代码示例
- 大数据可视化类型有哪些
- 【Struts1】--beanutils
- 《菜菜的机器学习sklearn课堂,孔浩Java视频百度云盘
- unity3d自定义Toggle组件,解决设置isOn自动调用方法
- 由于navigation引起viewwillappear不被调用
- k m kb mb计算机组成,为什么对计算机存储单位(K,M,G,T)换算,总是糊里又糊涂?
- 素数的几种求法(java)
- ubuntu 16.04 编译android 7.1,jack报错
- 机器学习中的数学——常用概率分布(一):伯努利分布(Bernoulli分布)
- Java编程工具有哪些比较实用
热门文章
- UBUNTU804VirtualBox出现常见问题解决(转高手的)我转的CU的
- 美国GIS的19个研究方向
- CodeForces - 1272E Nearest Opposite Parity(多源起点的最短路)
- POJ - 3415 Common Substrings(后缀数组+单调栈)
- 动态规划算法-06Longest Valid Parentheses问题
- Prufer序列相关
- PE文件结构详解(三)PE导出表
- Netty HTTP on Android
- Windows下查看端口被占用问题和解决办法
- MyBatis(五)MyBatis整合Spring原理分析