nav_path: best_practice


Go语言中的defer特性详解

在Go语言中,defer关键字用于预定函数或方法的执行,这常用于处理成对的操作如打开关闭文件、加解锁、记录时间等。defer的独特之处在于,无论包含它的函数通过何种路径返回,它都确保调用被defer的函数。

示例分析

示例1:defer在函数正常返回前执行

  1. // defer1.go
  2. package main
  3. import (
  4. "fmt"
  5. )
  6. func test1() {
  7. fmt.Println("test")
  8. }
  9. func main() {
  10. fmt.Println("main start")
  11. defer test1()
  12. fmt.Println("main end")
  13. }

执行结果:

  1. main start
  2. main end
  3. test

这里defer test1()确保了test1()main函数的最末尾执行。

示例2:defer在函数因panic结束前执行

  1. // defer2.go
  2. package main
  3. import (
  4. "fmt"
  5. )
  6. func test1() {
  7. fmt.Println("test")
  8. }
  9. func test2() {
  10. panic(1)
  11. }
  12. func main() {
  13. fmt.Println("main start")
  14. defer test1()
  15. test2()
  16. fmt.Println("main end")
  17. }

执行结果:

  1. main start
  2. test
  3. panic: 1
  4. ...

尽管test2()触发了panicdefer test1()依然得到了执行。

探究:defer是否总会执行?

现在考虑以下代码:

  1. // defer3.go
  2. package main
  3. import (
  4. "fmt"
  5. "os"
  6. )
  7. func test1() {
  8. fmt.Println("test")
  9. }
  10. func main() {
  11. fmt.Println("main start")
  12. defer test1()
  13. fmt.Println("main end")
  14. os.Exit(0)
  15. }

执行结果是:

  1. main start
  2. main end

defer的test1()并未执行。为何会这样?

这是因为os.Exit直接退出当前程序,不会执行任何已经defer的函数。

defer的四个基本原则回顾

  1. defer后必须是函数或方法调用,不能添加括号。
  2. defer的函数参数在执行defer表达式时固定。
  3. defer的执行顺序是后进先出(LIFO)。
  4. defer可以修改所属函数的命名返回值。

Go语言的defer实现原理

  1. type _defer struct {
  2. siz int32 // 参数和返回值的内存大小
  3. started bool
  4. heap bool // 是否分配在堆上
  5. openDefer bool // 是否进行了优化
  6. sp uintptr // 栈指针
  7. pc uintptr // 程序计数器
  8. fn *funcval // 被defer的函数
  9. _panic *_panic // defer的panic信息
  10. link *_defer // defer链表
  11. }

Go语言内部通过一个链表维护defer调用,这也解释了为何它们会按LIFO顺序执行。

总结

在Go中使用defer需要记住:并非所有情况下defer都会执行。例如,os.Exit会导致程序立即退出,此时defer不会被调用。了解defer的原理和限制能帮助我们更合理地编写Go代码。