golang defer和return深入理解

结论:defer和return的执行顺序

先为返回值赋值,然后执行defer,然后return到函数调用处。

一、先看return的使用方法

package main
 
import "fmt"
 
// return语句执行步骤
// 1、返回值赋值
// 2、defer语句
// 3、真正RET返回
func f0() (x int) {
	x = 5
	defer func() {
		x++
	}()
	return x //返回值RET=x, x++, RET=x=6
}
 
func f1() int {
	x := 5
	defer func() {
		x++ //修改的是x,不是返回值
	}()
	return x //返回值RET=5, x++, RET=5
}
 
func f2() (x int) {
	defer func() {
		x++
	}()
	return 5 //返回值RET=x=5, x++, RET=6
}
 
func f3() (y int) {
	x := 5
	defer func() {
		x++
	}()
	return x //返回值RET=y=x=5, x++, RET=5
}
 
func f4() (x int) {
	defer func(x int) {
		x++
	}(x)
	return 5 //返回值RET=x=5, x`++, RET=5
}
 
func main() {
	fmt.Println(f0()) //6
	fmt.Println(f1()) //5
	fmt.Println(f2()) //6
	fmt.Println(f3()) //5
	fmt.Println(f4()) //5
} 

二、先看一个例子,查看defer的效果

func foo() int {
    i := 0
    defer fmt.Println("in defer :", i)
    i = 1000
    fmt.Println("in foo:", i)
    return i+24
}

这段代码打印出:

in foo: 1000
in defer : 0
in main func: 1024

变量i初始化为0defer指定fmt.Println函数延迟到return后执行,最后main函数调用foo打印返回值。

三、一个函数中多个defer的执行顺序是什么?

defer关键字会使其以下的代码先执行后再执行它指定的函数,包括其下的defer语句也会比其先执行,依此类推。

这个顺序非常必要,因为在函数中,后面定义的对象可能依赖前面的对象,否则如果先出现的defer执行了,很可能造成后面的defer执行的时候出现异常。

所以,Go语言设计defer的时候是按先进后出的顺序执行的

func foo() {
    i := 0
    defer func() {
        i--
        fmt.Println("第一个defer", i)
    }()

    i++
    fmt.Println("+1后的i:", i)

    defer func() {
        i--
        fmt.Println("第二个defer", i)
    }()

    i++
    fmt.Println("再+1后的i:", i)

    defer func() {
        i--
        fmt.Println("第三个defer", i)
    }()

    i++
    fmt.Println("再+1后的i:", i)
}

运行后可以看到:

+1后的i: 1
再+1后的i: 2
再+1后的i: 3
第三个defer 2
第二个defer 1
第一个defer 0

四、当传递参数给defer指定的函数时,函数延迟执行,那么参数值会是多少?

defer指定的函数的参数在 defer 时确定,但,这只是一个总结,真正的原因是, Go语言除了map、slice、chan都是值传递

func foo() {
    i := 0
    defer func(k int) {
        fmt.Println("第一个defer", k)
    }(i)

    i++
    fmt.Println("+1后的i:", i)

    defer func(k int) {
        fmt.Println("第二个defer", k)
    }(i)

    i++
    fmt.Println("再+1后的i:", i)

    defer func(k int) {
        fmt.Println("第三个defer", k)
    }(i)

    i++
    fmt.Println("再+1后的i:", i)
}

得到的结果:

+1后的i: 1
再+1后的i: 2
再+1后的i: 3
第三个defer 2
第二个defer 1
第一个defer 0 

可能会有人觉得有一点出乎预料,i在return时不是已经被计算到3了吗?,为什么延迟执行的defer指定的函数里的i不是3呢?

defer关键字指定的函数是在return后执行的,这很容易让人想象在return后调用函数。

但是,defer指定的函数是在当前行就调用了的,只是延迟return后执行,而不等同于“移动”到return后执行,因此调用时传递的是当前的参数的值

五、传递指针参数会是什么情况?

如果希望defer指定的的函数参数的值是经过后面的代码处理过的,可以传递指针参数给defer指定的函数。

func foo() {
    i := 0
    defer func(k *int) {
        fmt.Println("第一个defer", *k)
    }(&i)

    i++
    fmt.Println("+1后的i:", i)

    defer func(k *int) {
        fmt.Println("第二个defer", *k)
    }(&i)

    i++
    fmt.Println("再+1后的i:", i)

    defer func(k *int) {
        fmt.Println("第三个defer", *k)
    }(&i)

    i++
    fmt.Println("再+1后的i:", i)
}

运行后得到:

+1后的i: 1
再+1后的i: 2
再+1后的i: 3
第三个defer 3
第二个defer 3
第一个defer 3

六、defer会影响返回值吗?

在开头的第一个例子中可以看到,defer是在foo执行完,main里打印返回值之前执行的,但是没有影响到main里的打印结果。

这还是因为相同的原则 Go语言除了map、slice、chan都是值传递

func main() {

    fmt.Println("foo1 return :", foo1())
    fmt.Println("foot return :", foo2())

}

func foo1() int {

    i := 0

    defer func() {
        i = 1
    }()

    return i
}

func foo2() map[string]string {

    m := map[string]string{}

    defer func() {
        m["a"] = "b"
    }()

    return m
}

运行后,打印出: 

foo1 return : 0
foot return : map[a:b]  

两个函数不同之处在于的返回值的类型,foo1中,int类型return后,defer不会影响返回结果,但是在foo2中map类型是引用传递,所以defer会改变返回结果。

这说明,在return时,除了map、slice、chan,其他类型return时是将值拷贝到一个临时变量空间,因此,defer指定的函数内对函数内的变量的操作不会影响返回结果的。

还有一种情况,给函数返回值申明变量名,,这时,变量空间是在函数执行前申明出来,return时只是返回这个变量空间的内容,因此defer能够改变返回值

例如,改造一下foo1函数,给它的返回值申明一个变量名i

func foo1() (i int) {

	defer func() {
		i = i + 4

	}()

	return i
}

返回结果如下:

foo1 return : 4

返回值被defer指定的函数修改了。

七、defer在panic和recover处理上的使用

在Go语言里,defer有一个经典的使用场景就是recover.

在函数执行过程中,有可能在很多地方都会出现panicpanic后如果不调用recover,程序会退出,为了不让程序退出,我们需要在panic后调用recover,但,panic后的代码不会执行,recover是不可能在panic后调用,然而panic所在的函数内defer指定的函数可以执行,所以recover只能在defer指定的函数中被调用,并且只需要在1个defer指定的函数中处理。

例如:

func panicfunc() {
    defer func() {
        fmt.Println("before recover")
        recover()
        fmt.Println("after recover")
    }()

    fmt.Println("before panic")
    panic(0)
    fmt.Println("after panic")
}

打印出:

before panic
before recover
after recover
posted @ 2022-06-19 17:28  南昌拌粉的成长  阅读(688)  评论(0编辑  收藏  举报