Go 錯(cuò)誤處理篇(二):defer 語(yǔ)句及其使用

Go 語(yǔ)言中的類(lèi)沒(méi)有構(gòu)造函數(shù)和析構(gòu)函數(shù)的概念,處理錯(cuò)誤和異常時(shí)也沒(méi)有提供 try...catch...finally 之類(lèi)的語(yǔ)法,那當(dāng)我們想要在某個(gè)資源使用完畢后將其釋放(網(wǎng)絡(luò)連接、文件句柄等),或者在代碼運(yùn)行過(guò)程中拋出錯(cuò)誤時(shí)執(zhí)行一段兜底邏輯,要怎么做呢?
通過(guò) defer 關(guān)鍵字聲明兜底執(zhí)行或者釋放資源的語(yǔ)句可以輕松解決這個(gè)問(wèn)題。比如我們看 Go 內(nèi)置的 io/ioutil 包提供的讀取文件方法 ReadFile 實(shí)現(xiàn)源碼,其中就有 defer 語(yǔ)句的使用:
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
var n int64 = bytes.MinRead
if fi, err := f.Stat(); err == nil {
if size := fi.Size() + bytes.MinRead; size > n {
n = size
}
}
return readAll(f, n)
}
defer 修飾的 f.Close() 方法會(huì)在函數(shù)執(zhí)行完成后或讀取文件過(guò)程中拋出錯(cuò)誤時(shí)執(zhí)行,以確保已經(jīng)打開(kāi)的文件資源被關(guān)閉,從而避免內(nèi)存泄露。如果一條語(yǔ)句干不完清理的工作,也可以在 defer 后加一個(gè)匿名函數(shù)來(lái)執(zhí)行對(duì)應(yīng)的兜底邏輯:
defer func() {
// 執(zhí)行復(fù)雜的清理工作...
} ()
另外,一個(gè)函數(shù)/方法中可以存在多個(gè) defer 語(yǔ)句,defer 語(yǔ)句的調(diào)用順序遵循先進(jìn)后出的原則,即最后一個(gè) defer 語(yǔ)句將最先被執(zhí)行,相當(dāng)于「?!惯@個(gè)數(shù)據(jù)結(jié)構(gòu),如果在循環(huán)語(yǔ)句中包含了 defer 語(yǔ)句,則對(duì)應(yīng)的 defer 語(yǔ)句執(zhí)行順序依然符合先進(jìn)后出的規(guī)則。
由于 defer 語(yǔ)句的執(zhí)行時(shí)機(jī)和調(diào)用順序,所以我們要盡量在函數(shù)/方法的前面定義它們,以免在后面編寫(xiě)代碼時(shí)漏掉,尤其是運(yùn)行時(shí)拋出錯(cuò)誤會(huì)中斷后面代碼的執(zhí)行,也就感知不到后面的 defer 語(yǔ)句。
下面我們看一段簡(jiǎn)單的 defer 示例代碼:
package main
import "fmt"
func printError() {
fmt.Println("兜底執(zhí)行")
}
func main() {
defer printError()
defer func() {
fmt.Println("除數(shù)不能是0!")
}()
var i = 1
var j = 1
var k = i / j
fmt.Printf("%d / %d = %d\n", i, j, k)
}
在這段代碼中,我們定義了兩個(gè) defer 語(yǔ)句,并且是在函數(shù)最頂部,以確保異常情況下也能執(zhí)行。
在函數(shù)正常執(zhí)行的情況下,這兩個(gè) defer 語(yǔ)句會(huì)在最后一條打印語(yǔ)句執(zhí)行完成后先執(zhí)行第二條 defer 語(yǔ)句,再執(zhí)行第一條 defer 語(yǔ)句:

而如果我們把 j 的值設(shè)置為 0,則函數(shù)會(huì)拋出 panic:

表示除數(shù)不能為零。這個(gè)時(shí)候,由于 defer 語(yǔ)句定義在拋出 panic 代碼的前面,所以依然會(huì)被執(zhí)行,底層的邏輯是在執(zhí)行 var k = i / j 這條語(yǔ)句時(shí),遇到除數(shù)為 0,則拋出 panic,然后立即中斷當(dāng)前函數(shù) main 的執(zhí)行(后續(xù)其他語(yǔ)句都不再執(zhí)行),并按照先進(jìn)后出順序依次執(zhí)行已經(jīng)在當(dāng)前函數(shù)中聲明過(guò)的 defer 語(yǔ)句,最后打印出 panic 日志及錯(cuò)誤信息。
關(guān)于 panic 及其內(nèi)部執(zhí)行邏輯,學(xué)院君將在下一篇教程給大家介紹。
總結(jié)一下,Go 語(yǔ)言的 defer 語(yǔ)句相當(dāng)于 Java/PHP 中的析構(gòu)函數(shù)和 finally 語(yǔ)句的功效,常用于定義兜底邏輯,在函數(shù)執(zhí)行完畢后或者運(yùn)行拋出 panic 時(shí)執(zhí)行,如果一個(gè)函數(shù)定義了多個(gè) defer 語(yǔ)句,則按照先進(jìn)后出的順序執(zhí)行。
(本文完)
學(xué)習(xí)過(guò)程中有任何問(wèn)題,可以通過(guò)下面的評(píng)論功能或加入「Go 語(yǔ)言研習(xí)社」與學(xué)院君討論:
本系列教程首發(fā)在 geekr.dev,你可以點(diǎn)擊頁(yè)面左下角閱讀原文鏈接查看最新更新的教程。
