Defer--打扫战场

361 阅读7分钟

一、背景 刚开始接触Golang看到这个defer,以前在PHP中都没有这些操作的,但依然不妨碍PHP是世界上最好的开发语言,不接受任何反驳. 书归正传今天依然秉承What,Why,How的路子来说说我对defer的理解

二、WHAT

a) defer是什么 ? defer是Go语言提供的一种用于注册延迟调用的机制:让函数或语句可以在当前函数执行完毕后(包括通过returm 正常结束或者panic导致的异常结束)执行。在需要释放资源的场景非常有用的,可以很方便地在的数结束前做些清理操作。在打开资源语句的下一行,直接使用defer 就可以在函数返回前释放资源,可谓相当有效。

b) 每次 defer 语句执行的时候,会把函数“压栈”,函数参数会被复制下来: 当外层函数(注意不是代码块,如一个for循环块并不是外层函数)退出时,defer 函数按照定义 的顺序逆序执行;如果defer 执行的函数为nil,那么会在最终调用函数的时候产生panic.)

c) defer通常用于一些成对操作的场景: 打开连接/关闭连接、加锁/释放锁、 打开文件/关闭文件等。使用非常简单,直接使用关键字就行,例如如下demo

f,err := os.Open("hello.txt")
if err != nil {
fmt.Println(err)
}
if f != nil {
defer f.Close()
}

三、WHY

a) defer的执行顺序是什么 ? defer语句并不会马上执行,而是会进入一个栈,函数returm前,会按先进后出的顺序执行。也就是说,最先被定义的defer 语句最后执行。先进后出的原因是后面定义的函数可能会依赖前面的资源,自然要先执行:否则,如果前面先执行了,那后面函数的依赖就没有了,因而可能会出错。 在defer 函数定义时,对外部变量的引用有两种方式:函数参数、闭包引用。前者在defer定 义时就把值传递给defer, 并被cache 起来:后者则会在defier 函数真正调用时根据整个上下文确定参数当前的值。

b) defer是如何执行的? defer后面的函数在执行的时候,函数调用的参数会被保存起来,也就是复制了一份。 真正执行的时候,实际上用到的是这个复制的变量,因此如果此变量是个“值”,那么就和定义的时候是一致的。如果此变量是一个“引用”,那就可能和定义的时候不一致。

四、HOW

a) func main() {

    var whatever [3]struct{}

   for i := range whatever {

      defer func() {

         fmt.Println(i)

      }()

   }

}

b) return后的defer会被执行吗? 下面来看一个例子:

func main() {

defer func() {

fmt.Println("return 前")

}()

if true {

fmt.Println("return 中")

return

}

defer func() {

fmt.Println("return 后")

 

}()

}

返回是:

图片4后.png

显然return之后的defer是没有被执行的

因为return之后的函数是没有被注册的所有”return 后”没有被打印

c) defer的执行流程是什么 ?

d) defer的执行与return息息相关defer使用的不好了会掉进万米深坑,所以就需要了解return这个关键,一般return xx 时经过编译后实际上会生成三条指令:

1.返回值 = xx

2.调用defer函数

3.空的return 返回

e) 那么我现在就拆解一下看看

例一:

func f() (r int) {

t := 5

defer func() {

t = t + 5

}()

return t

}

打印出来的结果: 5

拆解的过程

func f() (r int) {

t := 5

//1. 赋值

r = 5

//2.defer被插入到赋值和返回值之间执行,这个例子的返回值r没有被修改过

defer func() {

t = t + 5

}()

//3.空的return

return t

}

解析:因为在第二步的时候并没有操作返回值,因此在main函数中调用f() 得到的值为5

例二:

func f() (r int) {

defer func(r int) {

r = r + 5

}(r)

return 1

}

打印出来的结果: 1

拆解的过程:

func f() (r int) {

//1.赋值

r = 1

//2.defer被插入到赋值和返回值之间执行,这个例子的返回值r没有被修改过

defer func(r int) {

r = r + 5

}(r)

//3.空的return

return 1

}

解析:因为在第二步改变的是传值进去的r 是形参的一个复制值,不会影响到实参 r 因此在main函数中调用的f()函数 返回值依然会是1

f) 哪些情况下defer是没有生效的?

i. 

图片4生效.png ii. 已上2个函数输出的结果为:

defer err

nil

f4和f5 err在定义时都会求值,不同的是f4是一个闭包(闭包在go看来就是一个匿名函数没有函数名,不能独立存在,但可以赋值于某个变量或者直接调用),它引用的变量err在执行的时候最终变成了defer err,而f5则是我们现实中比较容易犯的错,很容易写出这样的代码defer语句没有起作用导致线上的一些事故,所以要特别注意

g) 防患于未然的recover()

i. panic相信go语言的开发人员都有遇到过,painc会停掉当前正在执行的程序,而不只是当前线程.在这之前,它会有序的执行完当前线程defer列表的语句,但是其他线程里的defer不一定会执行到,所以在defer里定义一个recover语句是很有必要的,防止程序直接挂掉就可以起到类似PHP中的try...catch的效果,值得注意的是recover()只有在defer的函数中直接调用才会生效.

例如:

func main() {

defer fmt.Println("defer main")

var user = os.Getenv("USER_")

go func() {

defer func() {

fmt.Println("defer caller")

if err := recover(); err != nil {

fmt.Println("recover 成功捕捉到错误:", err)

}

}()

 

func() {

defer func() {

fmt.Println("defer 在这里")

}()

if user == "" {

panic("请设定下user 环境")

}

//此处不会被执行

fmt.Println("painc 后")

}()

}()

time.Tick(time.Second)

fmt.Println("main函数的最后")

}

执行后的结果是:

main函数的最后

defer main

defer 在这里

defer caller

recover 成功捕捉到错误: 请设定下user 环境

解析:代码中的painc最终会被recover捕捉到,这样的处理方式在一个http请求流程中会被用到,一次偶然的请求会出发某个bug,这时用recover()捕捉painc稳住主流程,不影响其他请求

ii. 再来看几个recover()的应用不生效的场景

func main() {

//1.这里不能生效,因为不能直接调用recover,返回nil

//recover()

//2.这里不能生效,在defer函数中调用才生效

//defer recover()

//3.多重的defer嵌套不能生效

//defer func() {

// defer func() {

// recover()

// }()

//}()

}

 

五、

六、源码

a) 让我们来看个简单的例子:

func main() {
a, b := 1, 2
ff(a, b)
}
func ff(a, b int) {
defer sum(a, b)
fmt.Printf("a:%d,b:%d\n", a, b)
}
func sum(a, b int) {
c := a + b
fmt.Println("相加结果为", c)
}

i. 输出的结果为:

a:1,b:2

相加结果为 3

当函数ff()执行完时最终会进入到deferreturn 函数

图片4deferreturn .png

图片4deferreturn 2.png

这个方法是遍历的defer的链表(defer链表aa()->bb()->cc(),三个函数调用,每个函数中都有defer操作即为defer链表),当链表为空时即停止

每次执行完一个被defered的函数后.都会将_defer的结构体从链表中删除并回收,所以_defer链会越来越短,只是为空

七、总结

a) defer使用好了确实能给我们实际开发过程中带来很多便利个人认为尤其是对错误的捕捉,一直以来go的err一直也是被诟病的一个点,代码逻辑中有N多的error,在某些程序初始化时的错误,更应该直接painc避免上线后出现更大问题

八、最后

大哥说操作不规范,战友两行泪