go defer原理

原文链接:https://studygolang.com/articles/16067?tdsourcetag=s_pcqq_aiomsg

本文整理

defer语句用于延迟函数的调用,每次defer都会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行。

栗子:

func deferFuncParameter() {    
    var aInt = 1
    defer fmt.Println(aInt)
    aInt = 2
    return
}

输出1。延迟函数fmt.Println(aInt)的参数在defer语句出现时就已经确定了,所以无论后面如何修改aInt变量都不会影响延迟函数。

package main
import "fmt"
func printArray(array *[3]int) {    
    for i := range array {
        fmt.Println(array[i])
    }
}
func deferFuncParameter() {
    var aArray = [3]int{1, 2, 3}    
    defer printArray(&aArray)

    aArray[0] = 10
    return
}

func main() {
    deferFuncParameter()
}

输出10、2、3三个值。延迟函数printArray()的参数在defer语句出现时就已经确定了,即数组的地址,由于延迟函数执行时机是在return语句之前,所以对数组的最终修改值会被打印出来。

func deferFuncReturn() (result int) {    
    i := 1

    defer func() {
       result++
    }()    
    return i
}

函数输出2。函数的return语句并不是原子的,实际执行分为设置返回值–>ret,defer语句实际执行在返回前,即拥有defer的函数返回过程是:设置返回值–>执行defer–>ret。所以return语句先把result设置为i的值,即1,defer语句中又把result递增1,所以最终返回2。

defer规则

规则一:延迟函数的参数在defer语句出现时就已经确定下来了

func a() {    
    i := 0
    defer fmt.Println(i)
    i++
    return
}

defer语句中的fmt.Println()参数i值在defer出现时就已经确定下来,实际上是拷贝了一份。后面对变量i的修改不会影响fmt.Println()函数的执行,仍然打印"0"。

注意:对于指针类型参数,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下,defer后面的语句对变量的修改可能会影响延迟函数。

规则二:延迟函数执行按后进先出顺序执行,即先出现的defer最后执行

规则三:延迟函数可能操作主函数的具名返回值

函数返回过程:
func deferFuncReturn() (result int) {    
    i := 1

    defer func() {
       result++
    }()    
    return i
}

该函数的return语句可以拆分成下面两行:

result = i
return

而延迟函数的执行正是在return之前,即加入defer后的执行过程如下:

result = i
result++
return

所以上面函数实际返回i++值。

主函数拥有匿名返回值,返回字面值:
func foo() int {    
    var i int

    defer func() {
        i++
    }()    
    return 1
}

上面的return语句,直接把1写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。

主函数拥有具名返回值:
func foo() (ret int) {    
    defer func() {
        ret++
    }()    
    return 0
}

上面的函数拆解出来,如下所示:

ret = 0
ret++
return

defer原理

defer的数据结构:

type _defer struct {
    sp      uintptr   //函数栈指针
    pc      uintptr   //程序计数器
    fn      *funcval  //函数地址
    link    *_defer   //指向自身结构的指针,用于链接多个defer
}

defer后面一定要接一个函数的,所以defer的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。

与函数不同的一点是它含有一个指针,可用于指向另一个defer,每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。

[外链图片转存失败(img-DLIhqFHK-1567511255487)(C:\Users\linsw\AppData\Roaming\Typora\typora-user-images\1563344367628.png)]

源码包src/runtime/panic.go定义了两个方法分别用于创建defer和执行defer。

  • deferproc(): 在声明defer处调用,其将defer函数存入goroutine的链表中;
  • deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。

可以简单这么理解,在编译在阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。

上一篇:Go 语言基础——错误处理


下一篇:defer的基本使用.go