一、defer简单介绍
1、含义
defer有延迟的意思,在Golang中是一种延迟处理机制,它可以确保在函数结束之前执行某些特定的操作。
2、基本用法
// defer + 执行语句
func t(){
defer func(){
}()
defer fmt.println("1")
return
}
当使用defer关键词后,后面的执行语句将会被注册,用于函数return之前执行。
二、常见的使用场景
基于defer的这种机制,在函数结束之前无论发生任何异常defer都会被执行,所以我们会使用它来实现资源管理和异常捕获。
1、资源管理
当函数打开一些文件句柄、socket操作时,如果中间发生错误,很有可能无法执行相应的close操作,从而导致资源连接无法关闭,导致内存泄漏问题。在执行并发操作时,我们会用的互斥锁来进行同步操作,当加锁后,数据处理完成不及时解锁将导致其他协程无法获得锁。使用defer后无论中间业务逻辑出现什么问题,在函数执行结束后,资源将会被正确回收。
//文件管理
func createFile(){
file, err := os.Open()
defer file.Close()
}
//socket管理
func createsocket(){
socket , err := net.Listen("tcp",":8080")
defer socket.Close()
}
//锁的释放
func op(){
lock.Lock()
//todo
defer lock.Unlock()
}
2、异常捕获
panic是Go中的一个内建函数,用于触发一个运行时错误,当panic执行时开始逐级向上解包调用堆栈,直到被recover捕获,如果没有recover机制那么进程将直接崩溃。defer和recover配合将很好的捕获程序中的panic,防止进程运行时崩溃。
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered in main", r)
}
}()
ppanic()
}
func ppanic() {
panic("there is a panic!")
}
3、日志记录
由于defer的机制,不管是触发了panic还是函数正常结束,我们都可以将它记录下来,这样我们可以准备的得知当前函数发生了哪些事情。
func someting(){
defer func() {
if r := recover(); r != nil {
//如果panic了就打印panic
log.Printf("Frecovered from panic: %v", r)
} else {
//否则就记录一下事情
log.Printf("someting")
}
}()
}
4、多个defer的执行顺序
golang的defer是按照先进后出的方式来执行的,所以在代码中,最后注册(书写)的defer,将在最后执行。
package main
import "fmt"
func main() {
defer f1()
defer f2()
defer f3()
}
func f1() {
fmt.Println("this is f1")
}
func f2() {
fmt.Println("this is f2")
}
func f3() {
fmt.Println("this is f3")
}
//输出
/*
this is f3
this is f2
this is f1
*/
5、defer和return
我们都知道,在编程时return是函数的最后一步,而defer也是流程中最后一步执行,那么当defer遇到return时谁会最后执行呢。
首先我们先来了解一个概念,在Go中return并不是一个原子性操作,它分为赋值和返回两步操作,当return x或者某个变量时,首先会将x的值复制到return中的变量中,比如我们叫RET =X ,然后执行 return RET返回变量值。当与defer交互时,整个流程就变成了以下三步。
- set RET = X(某个变量)
- defer
- return RET
在Golang中函数的返回值有两种书写模式,分别是匿名和有名两种。我们分别按照上述的流程来看一下。
5.1 匿名模式
package main
import "fmt"
func main() {
r := f1()
fmt.Println("main value is ", r) //main value is 0
}
func f1() int {
var i int
defer func() {
i++
fmt.Println("value i is ", i) //value i is 1
}()
return i
}
//output
value i is 1
main value is 0
按照上面的三步走流程,可以很清晰的解析这个输出。
- set RET = i, 此时i未被初始化赋值,所以此时的RET = 0
- 执行defer i++ , 输出line 14打印
- 执行return RET , line 8 打印结果 0
5.2 有名模式
package main
import "fmt"
func main() {
r := f1()
fmt.Println("main value is ", r) //main value is 1
}
func f1() (i int) {
defer func() {
i++
fmt.Println("value i is ", i) //value i is 1
}()
return i
}
我们再来通过三步走流程解释一下
- set RET = i , 此时 i = 0
- 执行defer , line 13 打印 value is 1 , 并且此时i = 1 , RET = i= 1
- 执行return RET , line 8 打印 value is 1
三、使用defer的一些原则
1、不要使用defer来控制程序正常代码的流程
多个defer执行时有制定好的执行顺序,但是不要通过这个执行顺序来控制正常的代码。永远记住,defer主要是用来资源管理和异常管理的。
2、同一函数内尽量不要使用多个defer
多个defer执行时,会有执行顺序,但这样也会使得代码难以理解和维护,这也是1中为什么这么建议,不要使用多余的defer,能合并的代码都在同一个defer中执行,降低代码的理解难度。
四、总结
这是Golang中经常会碰到的一道面试题,主要考察对于defer机制的了解程度,我们在平时使用中,对于recover和资源回收会更加了解和熟悉,通过这篇文章希望大家能够多多了解defer的其他应用场景以及defer和return之间复杂的关系。
我是烤鱼,后面会给大家带来更多面试题的解答,求点赞、收藏、关注。