Go defer详解
什么是defer
在进行编程的时候,经常需要在代码中申请一些资源,比如数据库连接,文件句柄等等,这些资源需要在用完之后进行释放,否则会造成内存泄漏(内存一直被占用而不被释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果),而Go就提供了一个defer关键字,可以实现延迟调用。defer可以让函数或者语句在当前函数执行完毕后执行(包括程序正常退出和panic导致的异常结束)
以下代码有什么问题?
r.mu.Lock()
rand.Intn(param)
r.mu.Unlock()
看起来这段代码是没有什么问题的,但这是建立在程序正常运行的基础上,如果rand.Intn出现了异常而造成了panic,就会出现r一直占用着锁而不释放的问题,所以即使是简单的释放资源的代码,使用defer也是很有必要的。
defer的执行顺序
defer的语句并不会马上执行,而是会进入一个栈,所以执行的顺序也会和栈一样,即先进后出,也就是说最先被定义的defer语句最后执行,这个原因是后面定义的函数可能对前面的资源有依赖,所以要先执行;否则如果前面先执行了,对某些资源进行释放,后面的函数就会出错。
在defer函数定义时,对外部变量的引用有两种方式,函数参数和闭包引用,前者在defer定义时就把值(广义的值,如果传的是引用类型,那么和定义的时候可能不一样)传给defer,并被存起来,而后者则会在defer真正调用的时候根据上下文确定值。
如果是闭包的话,则会和定义的时候不一样,看下面这个栗子
1 | func main() { |
注意:在Go使用close()关闭某些资源的时候,最好提前判断调用主体是否为空,否则很可能会解引用一个空指针,从而造成panic的问题
return与defer
return并不是一条原子指令,return的执行顺序是这样的
1.返回值=xxx
2.调用defer函数
3.ret指令
下面是两道defer经典题
1 | func f1() int { //返回值没有取名 |
1 | func main() { |
什么是闭包
闭包=函数+引用环境 不推荐在生产环境中使用,就算你自己对闭包的使用很熟练,但是代码写出来是给人看的,不能苛求你的同事、测试对这部分也很熟悉,而且闭包很容易出现错误。
Go中的所有匿名函数都是闭包程序
defer配合recover使用
执行初始化的时候出问题,最好直接panic掉,避免上线之后出现更大的问题,有些时候,需要从异常中恢复,比如服务器的严重问题产生了panic,这个时候需要在程序崩溃之前做一些扫尾工作,比如关闭客户端的连接。并且一个panic不应该影响整个服务器的运行,这时候就需要defer 和 recovery进行配合
defer配合recovery使用:recovery必须在defer的函数中 才是标准格式
比如这种
func main() {
defer f()
panic(404)
}
func f() {
if err := recover(); err != nil {
fmt.Println("recover")
return
}
}
或者采用匿名函数
func main() { defer func() { if err := recover(); err != nil { fmt.Println("recover") return } }() panic(404) }
但是注意,一定要在函数里,像这样是不行的
func main() { defer recover() panic(404) }//还是会panic
defer链是如何被执行的
前面我们说到过,一个函数中的defer语句是按照栈的顺序执行的,每一条defer语句都会创建一个_defer结构体,这些结构体以链表的形式挂载在G下(Goroutine)。
defer首先会调用deferporc函数,new一个_defer结构体,挂到G上,当然,调用new之前会从当前G绑定的P中的defer pool中取,如果没有的话则会去全局的defer pool中取,是在没有就新建一个,这是Go runtime的常规操作,也就是设置多级缓存,提高运行效率。
之后按照顺序,处理一个个_defer结构体,即完成了defer链的执行。