Go面试题(一):聊聊你理解的defer关键字

15001004756 · · 921 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

**大家好,我是小道哥。** defer关键字是我们工作中经常用到的go语言特性,也是面试官比较青睐的一个知识点,今天通过这篇文章带各位道友彻底掌握它。 ## defer两大特性 defer是golang中的一个关键字,它主要具有两大特性: * **延迟调用**: 在当前函数执行完成后调用执行。 ```golang func f1(){ defer fmt.Println("hello world") fmt.Println("hello defer!") } ``` 输出结果: ```shell $ go run main.go hello defer! hello world ``` * **后进先出**: 多个defer函数时,执行顺序为后进先出。 ```golang func f2(){ defer fmt.Println("hello 1!") defer fmt.Println("hello 2!") defer fmt.Println("hello 3!") fmt.Println("hello defer!") } ``` 输出结果 ```shell $ go run main.go hello defer! hello 3! hello 2! hello 1! ``` ## defer与return的执行顺序 defer与return的执行顺序,是面试时经常考察的一点,需要道友们好好理解。 首先,我们举个栗子,看如下情况下代码的输出结果。 ```golang 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 0 } func main(){ fmt.Println(f1()) fmt.Println(f2()) fmt.Println(f3()) } ``` 建议朋友们先思考下答案,再往后看。 ```shell $ go run main.go 1 5 0 ``` >张三道友心想:卧槽,开什么玩笑,难道不是 0、10、5... 好的,下面我们逐一分析一下: 这里我们需要先理解下return语句的执行顺序。 **return语句本身并不是一条原子指令**,它会先给返回值赋值,然后再是返回,如下 ```golang func f4() (r int) { return 1 } //执行过程: r:=1 //赋值 ret //执行返回 ``` 而在含defer表达式时,函数返回的过程是这样的: **先给返回值赋值,然后调用defer表达式,最后再是返回结果** 即对于f1()来讲 ```golang func f1() (r int){ defer func(){ r++ }() return 0 } //执行过程: r:=0 //赋值 r++ //defer ret //r=1 ``` 对于f2来讲 ```golang func f2() (r int) { t:=5 defer func() { t = t+5 }() return t } //执行过程 t:=5 r:=t t=t+5 //defer ret //r=5 ``` 对于f3()来讲,在defer的时候传参r,其实是一个值拷贝。 所以defer中对r的修改并不会影响返回值结果,助于理解把r换成t,结果是等同的,即等效为 ```golang func f3() (r int) { defer func(t int) { t = t+5 }(r) return 0 } //执行过程 r:=0 t = r, t = t +5 //defer ret // r=0 ``` ## defer的应用场景 ### 场景一:资源释放 我们在代码中使用资源时如:打开一个文件,很容易因为忘记释放或者由于逻辑上的错误导致资源没有关闭。这时候使用defer可以避免这种资源泄漏。不妨先看如下代码: ```golang file,_ := os.Open("test.txt") //process为业务逻辑处理 if err:=process(file);err!=nil { return } file.Close() ``` 上面的代码即存在一个严重的问题,如 err!=nil 直接return后,会使得file.close() 关闭资源的语句没有执行,导致资源泄漏。 且在经历了一串业务逻辑处理编写后,我们也很容易忘记关闭资源导致资源泄漏。所以应该牢记一个原则:在每个资源申请成功的后面都加上defer自动清理,不管该函数都多少个return,资源都会被正确的释放。 **正确的编写逻辑如下:** ```golang file,_ := os.Open("test.txt") defer file.Close() //process为业务逻辑处理 if err:=process(file);err!=nil { return } ``` ### 场景二:异常捕获 Golang中对于程序中的异常处理,没有try catch,但是有panic和recover。 当程序中抛出panic时,如果没有及时recover,会导致服务直接挂掉,造成很严重的后果,所以我们一般用recover来捕获异常。 ```golang func main(){ defer func(){ if ok:=recover();ok!=nil{ fmt.Println("recover") } }() panic("error") } ``` **上面两个场景是我们必需要熟知的**,当然还可以利用defer的特性优雅的实现一些类似于代码追踪、记录函数的参数和返回值等。 ### 场景三: 代码追踪 我们通过追踪程序进入或离开某个函数的信息,来测试此函数是否被执行。 ```golang func main(){ f1() f2() } func f1(){ defer trace_leave(trace_enter("f1()")) fmt.Println("f1()程序逻辑") } func f2(){ defer trace_leave(trace_enter("f2()")) fmt.Println("f2()程序逻辑") } func trace_enter(msg string) string{ fmt.Println("enter: ",msg) return msg } func trace_leave(msg string) { fmt.Println("leave: ",msg) } ``` 输出结果如下: ```shell $go run main.go enter: f1() f1()程序逻辑 leave: f1() enter: f2() f2()程序逻辑 leave: f2() ``` ### 场景四: 打印函数的参数和返回值 某函数的执行结果不符合预期,我们可以使用defer来一步到位的打印函数的参数和返回值,而非多处打印调试语句。 ```golang func main(){ func1("hello") } func func1(str string) ( res string) { defer func() { fmt.Printf("func1(%s) = %s", str, res) }() res = fmt.Sprintf("%s, jack!",str) return } ``` 输出结果: ```shell $go run main.go func1(hello) = hello, jack! ``` ## 面试点总结 - defer的两大特性 - defer与return的执行顺序 - defer的应用场景

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

921 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传