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. 参考

  1. https://github.com/golang/go/blob/release-branch.go1.12/src/runtime/panic.go#L229
  2. https://github.com/golang/go/blob/master/src/runtime/asm_amd64.s#L550
  3. 《Go 语言学习笔记》

最后,我之前只在博客 http://www.legendtkl.com 和知乎上(知乎专栏:Golang Inside)上面写文章,现在开始在公众号(公众号:legendtkl)上面尝试一下,如果你觉得不错,或者之前看过,欢迎关注或者推荐给身边的人。谢谢。

深入理解golang的defer相关推荐

  1. 理解Golang中defer的使用

    之前一直对Go中的defer不太理解,所以我单独弄出来整理一下. 在golang当中,defer代码块会在函数调用链表中增加一个函数调用.这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是r ...

  2. golang的defer的理解- defer的函数一定会执行吗?

    文章目录 golang的defer 什么是defer 理解defer defer什么时间执行(defer. return.返回值 三者的执行顺序) defer输出的值,就是定义时的值.而不是defer ...

  3. 深入理解Golang之context

    深入理解Golang之context context是Go并发编程中常用到一种编程模式.本文将从为什么需要context,深入了解context的实现原理,以了解如何使用context. 作者:Tur ...

  4. golang的defer机制详解

    defer概述 defer用来声明一个延迟函数,把这个函数放入到一个栈上,当外部的包含方法return之前,返回参数到调用方法之前调用,也可以说是运行到最外层方法体时调用.我们经常用他来做一些资源的释 ...

  5. 深入理解Golang 编程思维和工程实战

    | 导语 Golang 的一些编程思维和思想,以及总结一些常见的优雅编程实战技巧 目录 一 Golang 编程思维 二 Golang 高级编码技巧 1 优雅的实现构造函数编程思想 2 优雅的实现继承编 ...

  6. 深入理解Golang中的Context包

    context.Context是Go语言中独特的设计,在其他编程语言中我们很少见到类似的概念.context.Context深度支持Golang的高并发. 1. Goroutine和Channel 在 ...

  7. 理解Golang的Time结构

    在golang中创建并打印一个时间对象,会看到如下输出 2018-10-26 14:15:50.306558969 +0800 CST m=+0.000401093 复制代码 前面表示的意义好理解,分 ...

  8. 深入理解Golang包导入

    Golang使用包(package)这种语法元素来组织源码,所有语法可见性均定义在package这个级别,与Java .python等语言相比,这算不上什么创新,但与C传统的include相比,则是显 ...

  9. Go实战--golang中defer的使用

    原址 生命不止,继续 go go go !!! 学习golang这么久了,还没看到类似传统的 try-catch-finally 这种异常捕捉方式.  但是,Go中引入的Exception处理:def ...

最新文章

  1. Android NDK基础样例
  2. Lync Server 2010的部署系列_第三章 证书、架构、DNS规划
  3. 使用Docker-数据卷命令
  4. java 支付宝 退款_Java 支付宝支付,退款,单笔转账到支付宝账户(支付宝支付)
  5. 小猫的java基础知识点汇总(上)
  6. 2019快手内容报告重磅发布:日活突破3亿 点赞量超3500亿
  7. Homebrew命令具体解释
  8. ActiveMQ 权限
  9. 暴力推荐2:硬盘分区丢失之DiskGenius
  10. 通过Cookie存取用户游览记录的代码示例
  11. 大数据可视化类型有哪些
  12. 【Struts1】--beanutils
  13. 《菜菜的机器学习sklearn课堂,孔浩Java视频百度云盘
  14. unity3d自定义Toggle组件,解决设置isOn自动调用方法
  15. 由于navigation引起viewwillappear不被调用
  16. k m kb mb计算机组成,为什么对计算机存储单位(K,M,G,T)换算,总是糊里又糊涂?
  17. 素数的几种求法(java)
  18. ubuntu 16.04 编译android 7.1,jack报错
  19. 机器学习中的数学——常用概率分布(一):伯努利分布(Bernoulli分布)
  20. Java编程工具有哪些比较实用

热门文章

  1. UBUNTU804VirtualBox出现常见问题解决(转高手的)我转的CU的
  2. 美国GIS的19个研究方向
  3. CodeForces - 1272E Nearest Opposite Parity(多源起点的最短路)
  4. POJ - 3415 Common Substrings(后缀数组+单调栈)
  5. 动态规划算法-06Longest Valid Parentheses问题
  6. Prufer序列相关
  7. PE文件结构详解(三)PE导出表
  8. Netty HTTP on Android
  9. Windows下查看端口被占用问题和解决办法
  10. MyBatis(五)MyBatis整合Spring原理分析