Go 编程 | 连载 25 - Go 的 defer 语句

1,231 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情

一、defer 语句

Go 中的 defer 语句又叫做延迟执行语句,也就是说 defer 语句会将其后面根素的语句进行延迟处理。

多个 defer 语句的执行顺序

在 defer 关键字所在的函数即将返回时(返回前),将延迟处理的语句按 defer 的逆序进行执行,既先被 defer 的语句最后执行,最后被 defer 的语句最先执行,在被 defer 修饰时遵循 先进后出 原则.

package main

import "fmt"

func main() {

   fmt.Println("defer 修饰语句的执行顺序为 `先进后出`")

   defer fmt.Println("第一个进入 defer 栈,最会出(执行)")
   defer fmt.Println("第二个进入 defer 栈,倒数第二个出(执行)")
   defer fmt.Println("第三个进入 defer 栈,倒数第三个出(执行)")

   fmt.Println("defer 语句会在所属函数返回前执行:")
}

执行上述代码,输出结果如下:

defer 修饰语句的执行顺序为 `先进后出`
defer 语句会在所在函数返回前执行:
第三个进入 defer 栈,倒数第三个出(执行)
第二个进入 defer 栈,倒数第二个出(执行)
第一个进入 defer 栈,最会出(执行)

二、defer 语句在函数退出时释放资源

在文件处理和连接数据库时都会遇到 打开关闭 这种成对的操作,Go 中没有 try-catch-finally 语句可以在执行 finally 代码块中执行关闭操作。

Go 中可以通过 defer 语句来代替 finally 实现资源关闭的操作,同时也能解决 try-catch-finally 的大量嵌套。

以文件打开和关闭为例,首先创建一个 info.txt 文件,Go 中打开和关闭文件需要使用到 os 包中的 OpenClose 函数。

package main

import (
   "fmt"
   "log"
   "os"
)

func main() {

   fmt.Println(getFileSize("info.txt")) # 90

}

func getFileSize(path string) (size int) {
   f, err := os.Open(path)
   if err != nil {
      log.Fatal(err)
   }

   // 延迟关闭文件
   defer f.Close()

   fileInfo, err := f.Stat()
   if err != nil {
      log.Fatal(err)
   }
   size = int(fileInfo.Size())

   return
}

image.png

需要注意的是 defer 后的表达式必须是函数调用。

三、defer 机制的细节

defer 语句执行时的拷贝机制,既会执行目标函数的副本,当原函数改变后,defer 执行不受影响。

func main() {

   zulu := func() {
      fmt.Println("Go")
   }

   defer zulu()

   zulu = func() {
      fmt.Println("Elixir")
   }
}

执行上述代码,输出结果如下:

Go

defer 后的函数入参为值类型时,会将参数一同拷贝,修改参数不会影响 defer 后函数的执行结果。

func main() {

   x := 6

   zulu := func(x int) {
      fmt.Println(x)
      fmt.Println("Go")
   }

   defer zulu(x)

   x = 16
}

执行上述代码,输出结果如下:

6
Go

将 defer 后 zulu 变量中函数的参数改为引用传递时,如果原参数改变,对 defer 后函数的执行结果也会有影响。

func main() {

   x := 6
   xPtr := &x

   zulu := func(x *int) {
      fmt.Println(*x)
      fmt.Println("Go")
   }

   defer zulu(xPtr)

   x = 16
}

执行上述代码,输出结果如下:

16
Go

与 finally 做对比

func main() {

   fmt.Println(f1()) # 10

}

func f1() int {
   x := 10

   defer func(){
      x++
   }()

   return x
}

返回的值并未收到影响。

将函数反射值设置为引用类型

func main() {

   fmt.Println(*f1())

}

func f1() *int {
   x := 10
   xPtr := &x

   defer func(){
      *xPtr++
   }()

   tmp := xPtr

   return tmp
}

再次执行,输出结果如下:

11

输出结果受到影响。

函数 return 前会重新定义一个变量并赋值,返回的是新定义的变量,如果是值类型赋值就重新拷贝一个,对前面定义的变量没有影响,如果使用引用数据类型,这两个变量执行的是同一个内存地址,数据改变了都会跟着改变。