4

深入理解 Go | defer

 2 years ago
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.
neoserver,ios ssh client

以下基于 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),从而避免运行时的 deferprocdeferprocStack 操作,以及多次调用 deferreturn

  • 以下情况不使用这种类型来处理 defer
    • 函数中对 defer 的调用次数超过 8(这是为了最小化代码大小,只使用 1 个 byte 来辅助标识)(例如下面的 f0f1
    • 函数中存在出现在循环中的 defer(例如下面的 f2f3f4f5
      • 包括使用 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,直到所有的延迟调用都执行完毕。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK