深入理解 Go | defer
source link: https://ictar.github.io/2020/03/25/dive-into-go-defer/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
以下基于 Go 1.14
Go 语言中的 defer
常用来进行资源释放。它有以下几个特点: * 向 defer
传入的函数会在当前函数或者方法返回之前运行。 * 函数中调用的多个 defer
会以先调用后执行的方式进行 * 在调用 defer
时,就会对函数传入的参数进行计算。
defer
类型
有三种类型的 defer
编译器的 ssa 过程中会确认当前 defer
的类型:
// compile.internal.gc.state.stmt
func (s *state) stmt(n *Node) {
//...
switch n.Op {
// ...
case ODEFER:
// ...
if s.hasOpenDefers {
s.openDeferRecord(n.Left)
} else {
d := callDefer
if n.Esc == EscNever {
d = callDeferStack
}
s.call(n.Left, d)
}
}
// ...
}
> 可以使用 $ go tool compile -d defer hello.go
来检查 defer
类型。
open-coded
Go 1.14 引入,目的是优化 defer 的运行时间。编译器在 ssa 过程中,会将被延迟的方法直接插入到函数的尾部(inline),从而避免运行时的 deferproc
和 deferprocStack
操作,以及多次调用 deferreturn
。
- 以下情况不使用这种类型来处理
defer
:- 函数中对
defer
的调用次数超过 8(这是为了最小化代码大小,只使用 1 个 byte 来辅助标识)(例如下面的f0
和f1
) - 函数中存在出现在循环中的
defer
(例如下面的f2
、f3
、f4
和f5
)- 包括使用
for
构造的和使用 label+goto
构造的
- 包括使用
- 函数中出现过多(返回语句次数 * defer 个数 > 15)的返回语句
- 因为会在每个返回点前插入被 defer 的函数调用
gcflags
无 N
- 函数中对
举例说明:
// 8 次 defer
func f0() {
defer func() { // open-coded defer
fmt.Println("defer0")
}()
defer func() { // open-coded defer
fmt.Println("defe1")
}()
defer func() { // open-coded defer
fmt.Println("defer2")
}()
defer func() { // open-coded defer
fmt.Println("defe3")
}()
defer func() { // open-coded defer
fmt.Println("defer4")
}()
defer func() { // open-coded defer
fmt.Println("defe5")
}()
defer func() { // open-coded defer
fmt.Println("defer6")
}()
defer func() { // open-coded defer
fmt.Println("defer7")
}()
fmt.Println("f0")
}
func f1() { // 9 次 defer
defer func() { // stack-allocated defer
fmt.Println("defer0")
}()
defer func() { // stack-allocated defer
fmt.Println("defe1")
}()
defer func() { // stack-allocated defer
fmt.Println("defer2")
}()
defer func() { // stack-allocated defer
fmt.Println("defe3")
}()
defer func() { // stack-allocated defer
fmt.Println("defer4")
}()
defer func() { // stack-allocated defer
fmt.Println("defe5")
}()
defer func() { // stack-allocated defer
fmt.Println("defer6")
}()
defer func() { // stack-allocated defer
fmt.Println("defer7")
}()
defer func() { // stack-allocated defer
fmt.Println("defer8")
}()
fmt.Println("f1")
}
func f2() { // defer 没有出现在循环中(for)
defer func() { // open-coded defer
fmt.Println("defer0")
}()
for i := 0; i < 1; i += 1 {
fmt.Println("f2", i)
}
}
func f3() { // defer 出现在循环中(for)
for i := 0; i < 1; i += 1 {
defer func() { // heap-allocated defer
fmt.Println("defer0")
}()
}
fmt.Println("f3")
}
func f4() { // defer 没有出现在循环中(label+goto)
defer func() { // open-coded defer
fmt.Println("defer0")
}()
label:
fmt.Println("f4")
goto label
}
func f5() { // defer 出现在循环中(label+goto)
label:
defer func() { // heap-allocated defer
fmt.Println("defer0")
}()
fmt.Println("f5")
goto label
}
### stack-allocated Go 1.13 引入,用于优化性能,表示在栈上分配 defer
相关的结构体
那么,什么时候会在栈上分配呢?答案在下面这部分代码:
// src/cmd/compile/internal/gc/escape.go
func (e *Escape) augmentParamHole(k EscHole, call, where *Node) EscHole {
// ...
// Top level defers arguments don't escape to heap, but they
// do need to last until end of function. Tee with a
// non-transient location to avoid arguments from being
// transiently allocated.
if where.Op == ODEFER && e.loopDepth == 1 {
// force stack allocation of defer record, unless open-coded
// defers are used (see ssa.go)
where.Esc = EscNever
return e.later(k)
}
// ...
}
举例说明:
func f6() {
defer func() { // stack-allocated defer
fmt.Println("defer2")
}()
for {
defer func() { // heap-allocated defer
fmt.Println("defer1")
}()
break
}
}
### heap-allocated 表示在堆上分配 defer
相关的结构体,最原始的方式。
一个数据结构
在 Go 中,defer
关键字对应的数据结构为 runtime._defer
。这是一个用链表实现的栈。
- 处理
defer
关键字- 如果是
open-coded
类型的 defer,则调用cmd/compile/internal/gc.state.openDeferRecord
方法, - 如果是
stack-allocated
类型,则转换成runtime.deferprocStack
- 如果是
heap-allocated
类型,则转换成runtime.deferproc
- 如果是
- 在调用
defer
的函数返回之前插入runtime.deferreturn
### 运行时// compile.internal.gc.state
// 处理任何需要在返回前生成的代码
func (s *state) exit() *ssa.Block {
if s.hasdefer { // 函数中存在 defer 调用
if s.hasOpenDefers { // 如果有 open-coded 类型的 defer
if shareDeferExits && s.lastDeferExit != nil && len(s.openDefers) == s.lastDeferCount {
if s.curBlock.Kind != ssa.BlockPlain {
panic("Block for an exit should be BlockPlain")
}
s.curBlock.AddEdgeTo(s.lastDeferExit)
s.endBlock()
return s.lastDeferFinalBlock
}
s.openDeferExit()
} else { // 对于其他类型的 defer,调用
s.rtcall(Deferreturn, true, nil)
}
}
//...
}
// openDeferExit 生成 SSA,从而在退出的时候处理所有的 open-coded 类型的 defer。
// 这个过程会加载 deferBits 字段,然后检查这个字段的每个位,检查是否执行了对应的 defer 语句。
// 对于每一个打开的位,会进行相关的 defer 调用。
func (s *state) openDeferExit() {
// ...
} - 如果调用了
runtime.deferprocStack
或者runtime.deferproc
,那么它们都会将一个新的runtime._defer
结构体(此时就会对函数参数进行计算)追加到当前 Goroutine 的_defer
链表的头部 runtime.deferreturn
会从 Goroutine 的_defer
链表中取出runtime._defer
结构并执行- 如果是
open-coded
类型的延迟调用,则会调用runtime.runOpenDeferFrame
函数来运行该_defer
结构中所有有效的延迟调用。 - 否则,它会调用
runtime·jmpdefer
函数。这个函数会跳到对应被延迟调用的函数并执行 - 会多次调用
runtime.deferreturn
,直到所有的延迟调用都执行完毕。
- 如果是
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK