defer介绍

Go函数里提供了defer关键字,可以注册多个延迟调用,这些调用以先进后出的顺序在函数返回前被执行,defer常用于保证一些资源最终一定能够得到回收和释放。

package main

import fmt

func main() {
    // 先进后出
    defer func() {
        fmt.Println("first")
    }()
    defer func() {
        fmt.Println("second")
    }()

    fmt.Println("function")
}

结果

function
second
first

defer后面必须是函数或方法的调用,不能是语句。

defer函数的实参在注册时通过值拷贝传递进去,下面示例代码中,实参a的值在defer注册时通过值拷贝传递进去,后续a++并不会影响defer语句最后的输出结果。

func f() int {
    a := 0
    defer func(i int) {
        fmt.Println("defer i=", i)
    }(a)

    a ++
    return a
}

结果

defer i=0

defer语句必须先注册后才能执行,如果defer位于return之后,则defer因为没有注册,不会执行。

func main() {
    defer func() {
        fmt.Println("first")
    }()

    fmt.Println("function")
    return

    defer func() {
        fmt.Println("second")
    }()
}

结果

function
first

主动调用os.Exit(int)退出进程时,defer将不会再执行(即使defer已经提前注册)。

func main() {
    defer func() {
        fmt.Println("first")
    }()

    fmt.Println("function")

    os.Exit(1)
}

结果

function
exit status 1

defer的好处是可以在一定程度上避免资源泄漏,特别是在有很多return语句,有很多资源需要关闭的场景中,很容易漏掉资源的关闭操作。

defer陷阱

defer和函数返回值

defer中如果引用了函数的返回值,则因引用形式不同会导致不同的结果,我们先来看一下如下三个函数的执行结果:

package main

import fmt

func f1() (r int) {
    defer func() {
        r++
    }()
    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() {
    fmt.Println("f1=", f1())
    fmt.Println("f2=", f2())
    fmt.Println("f3=", f3())
}

结果:

f1=1
f2=5
f3=1

绝大多数人看到执行结果会很茫然,到底什么原因导致的这个结果?接下来我们逐个分析。f1f2f2三个函数的共同点就是它们都是带命名返回值的函数,返回值都是变量r,通过函数我们可以知道:

  • 函数调用方负责开辟栈空间,包括形参和返回值的空间
  • 有名的函数返回值相当于函数的局部变量,被初始化为类型的零值

现在我们来分析下f1defer语句后面的匿名函数是对函数返回值r的闭包引用,f1函数的逻辑如下:

  • r是函数的有名返回值,分配在栈上,其地址又被称为返回值所在栈区,首先r被初始化为0
  • return 0会复制0到返回值栈区,返回值r被赋值为0
  • 执行defer语句,由于匿名函数对返回值r是闭包引用,所以r++执行后,函数返回值被修改为1
  • defer语句执行完后RET返回,此时函数的返回值任然为1

同理来分析函数f2的逻辑:

  • 返回值r被初始化为0
  • 引入局部变量t,并初始化为5
  • 复制t的值5到返回值r所在的栈区
  • 执行defer语句,由于匿名函数是对局部变量t的闭包引用,t的值被设置为10
  • 函数返回,此时函数返回值栈区的值仍然是5

最后分析函数f3的逻辑:

  • 返回值r被初始化为0
  • 复制1到函数返回值的栈区
  • 执行deferdefer后匿名函数使用的是传参数调用,在注册defer函数时将函数返回值r作为实惨传进去,由于函数调用的是值拷贝,所以defer函数执行后知识形参值变为5,对实参没有任何影响
  • 函数返回,此时函数返回值栈区上的值是1

综上所述,对于带defer的函数返回整体上有三个步骤:

  • 执行return的值拷贝,将return语句返回的值复制到函数的值栈区(如果只有一个return,不带任何变量或值,则此步骤不做任何动作)
  • 执行defer语句,多个defer按照先进后出顺序执行
  • 执行RET指令

版权声明:如无特殊说明,文章均为本站原创,转载请注明出处

本文链接:https://www.ltfred.com/article/go-defer/