defer 是我們經常會使用的一個關鍵字,它會在當前函式返回前執行傳入的函式,常用於關閉檔案描述符、關閉資料庫連線以及解鎖資源。
使用場景
釋放資源
這是 defer 最常見的用法,包括釋放互斥鎖、關閉檔案控制程式碼、關閉網路連線、關閉管道和停止定時器等,如:
m.mutex.Lock()
defer m.mutex.Unlock()
異常處理
defer 第二個重要用途就是處理異常,與 recover 搭配一起處理 panic,讓程式從異常中恢復。例如 gin 框架中 recovery 中介軟體的原始碼:
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
// ...
修改命名返回值
// $GOROOT/src/fmt/scan.go
func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
defer func() {
if e := recover(); e != nil {
if se, ok := e.(scanError); ok {
err = se.err
} else {
panic(e)
}
}
}()
...
}
列印除錯資訊
// $GOROOT/src/net/conf.go
func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) {
if c.dnsDebugLevel > 1 {
defer func() {
print("go package net: hostLookupOrder(", hostname, ") = ", ret.String(), "\n")
}()
}
...
}
行為規則
defer 的語法很簡單,不過衍生出的用法很多,有時讓人迷惑,在這裡我們總結一下 defer 的幾個基本使用規則。
同一函式內部不同 defer 關鍵字後面的函式是逆序執行的
後面我們會討論,defer 關鍵字後面的延遲函式需要註冊到一個 deferred 函式棧中(本質上是一個連結串列),因此遵循棧的後進先出規則,多個 defer 後面的函式是逆序執行的。
defer 關鍵字後面的函式引數是在 defer 關鍵字出現時預計算的
defer 關鍵字後面的函式是在註冊到 deferred 函式棧的時候進行求值的。下面看一個例子:
func test1() {
for i := 0; i <= 3; i++ {
defer func(n int) {
fmt.Println(n)
}(i)
}
}
func test2() {
for i := 0; i <= 3; i++ {
defer func() {
fmt.Println(i)
}()
}
}
func main() {
test1()
test2()
}
在 test1 中,defer 後面接的是一個帶有一個引數的匿名函式。每當 defer 將匿名函式註冊到 deferred 函式棧的時候,都會對該匿名函式的引數進行求值。因此,deferred 函式棧中匿名函式的引數依次是 0,1,2,3,列印出來的結果就是 3,2,1,0。
在 test2 中,defer 後面接的是一個不帶引數的匿名函式。當程式碼執行的時候,deferred 棧中彈出的函式會以閉包的方式訪問外部變數 i,而此時 i 的值已經變為了 4,因此列印結果為 4,4,4,4。
實現原理
資料結構
// src/runtime/runtime2.go
type _defer struct {
siz int32
started bool
heap bool
openDefer bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
- siz 是引數和結果的記憶體大小;
- heap 表示該結構體是否存於堆中;
- sp 和 pc 分別代表棧指標和呼叫方的程式計數器;
- fn 是 defer 關鍵字中傳入的函式;
- _panic 是觸發延遲呼叫的結構體,可能為空;
- openDefer 表示當前 defer 是否經過開放編碼的優化。
可以看出,每個 _defer 例項實際上是對一個函式的封裝,擁有執行函式的必要資訊,如棧指標等。多個 _defer 例項使用指標 link 連線起來形成一個單向連結串列,儲存到當前 goroutine 的資料結構中,待當前函式執行結束再逐個取出執行。
type g struct {
// ...
_defer *_defer
// ...
}
執行機制
在中間程式碼生成階段會處理程式中的 defer,根據不同的條件,會有三種不同的機制來處理該關鍵字:堆分配、棧分配和開放編碼。早期的 Go 語言會在堆上分配 _defer 結構體,不過該實現的效能較差,在 1.13 版本中引入棧上分配的結構體,減少了 30% 的額外開銷,然後在 1.14 中引入了基於開放編碼的 defer,使得效能大幅度提升。
堆分配
堆分配是預設的兜底方案,採用本方案時,在編譯期間會將 defer 關鍵字轉換成 runtime.deferproc 函式,並且為所有呼叫 defer 的函式末尾插入 runtime.deferreturn 函式。簡而言之,就是 deferproc 生成 _defer 結構體並插入連結串列,deferreturn 取出 _defer 並執行。
來簡單看一下原始碼:
func deferproc(siz int32, fn *funcval) {
sp := getcallersp()
argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
callerpc := getcallerpc()
d := newdefer(siz)
if d._panic != nil {
throw("deferproc: d.panic != nil after newdefer")
}
d.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()
}
deferproc 會為 defer 建立一個新的 _defer 結構體、設定它的函式指標 fn、程式計數器 pc 和棧指標 sp 並將相關的引數拷貝到相鄰的記憶體空間中。
newdefer 的作用是獲取 _defer 結構體,這裡包含三種路徑:
- 從排程器的延遲呼叫快取池 sched.deferpool 中取出結構體並將該結構體追加到當前 Goroutine 的快取池中;
- 從 Goroutine 的延遲呼叫快取池 pp.deferpool 中取出結構體;
- 通過 runtime.mallocgc 在堆上建立一個新的結構體。
無論使用哪種方式,獲取到 _defer 結構體之後,都會被追加到所在 Goroutine 的 _defer 連結串列的最前面。
棧分配
棧分配 defer 是為了提高堆分配 defer 的記憶體使用效率而引入的,當 defer 關鍵字在函式中最多執行一次時,編譯器就會將 defer 編譯成 deferprocStack 函式將 _defer 結構體分配到棧上。
在編譯期間已經建立了 _defer 結構體,所以 deferprocStack 只需要設定一些未初始化的欄位,然後將棧上的 _defer 追加到連結串列上。
func deferprocStack(d *_defer) {
gp := getg()
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
return0()
}
開放編碼
無論是堆分配 defer 還是棧分配 defer,編譯器都只能把 defer 轉換成相應的建立 _defer 結構體的函式,最後通過 deferreturn 函式取出結構體再執行。如果編譯器不這麼麻煩,直接把 defer 語句轉換成相應的程式碼插入函式尾部,是不是就可以節省很多步驟,提高儲存效率和效能?開放編碼使用的就是這種思路。
但並不是所有的情況下都可以使用開放編碼方式,在一下場景下 defer 語句不能被處理成開放編碼型別:
- 編譯時禁用了編譯器優化;
- defer 出現在迴圈中;
- 單個函式中 defer 出現了 8 個以上;
- 單個函式中 return 語句的個數乘以 defer 語句的個數超過了 15。