????????延遲語句(defer)是Go 語言里一個非常有用的關鍵字,它能把資源的釋放語句與申請語句放到距離相近的位置,從而減少了資源泄漏的情況發生。
延遲語句是什么
????????defer 是Go 語言提供的一種用于注冊延遲調用的機制:讓函數或語句可以在當前函數執行完畢后(包括通過return 正常結束或者panic 導致的異常結束)執行。在需要釋放資源的場景非常有
用,可以很方便地在函數結束前做一些清理操作。在打開資源語句的下一行,直接使用defer 就可
以在函數返回前釋放資源,可謂相當有效。
defer 通常用于一些成對操作的場景:打開連接/關閉連接、加鎖/釋放鎖、打開文件/關閉文件
等。使用非常簡單:?
f,err := os.Open(filename)if err != nil {panic(err)}if f != nil {defer f.Close()}
????????在打開文件的語句附近,用defer 語句關閉文件。這樣,在函數結束之前,會自動執行defer
后面的語句來關閉文件。注意,要先判斷f 是否為空,如果f 不為空,再調用f.Close()函數,避免出
現異常情況。
當然,defer 會有短暫延遲,對時間要求特別高的程序,可以避免使用它,其他情況一般可以忽略它帶來的延遲。特別是Go 1.14 又對defer 做了很大幅度的優化,效率提升了不少。
我們舉一個反面例子:?
r.mu.Lock()
rand.Intn(param)
r.mu.Unlock()
????????上面只有三行代碼,看起來這里不用 defer 執行 Unlock 并沒有什么問題。其實并不是這樣,
中間這行代碼rand.Intn(param)其實是有可能發生 panic 的,更嚴重的情況是,這段代碼很有可能被其他人修改,增加更多的邏輯,而這完全不可控。也就是說,在 Lock 和 Unlock 之間的代碼一旦出現異常情況導致 panic,就會形成死鎖。因此這里的邏輯是,即使是看起來非常簡單的代碼,使用 defer 也是有必要的,因為需求總是在變化,代碼也總會被修改。?
延遲語句的執行順序是什么?
????????每次 defer 語句執行的時候,會把函數“壓棧”,函數參數會被復制下來;當外層函數(注意不是代碼塊,如一個 for 循環塊并不是外層函數)退出時,defer 函數按照定義的順序逆序執行;如果 defer 執行的函數為nil,那么會在最終調用函數的時候產生 panic?
????????defer 語句并不會馬上執行,而是會進入一個棧,函數return 前,會按先進后出的順序執行。也就是說,最先被定義的defer 語句最后執行。先進后出的原因是后面定義的函數可能會依賴前面的資源,自然要先執行;否則,如果前面先執行了,那后面函數的依賴就沒有了,因而可能會出錯。
在 defer 函數定義時,對外部變量的引用有兩種方式:函數參數、閉包引用。前者在 defer 定
義時就把值傳遞給 defer,并被 cache 起來;后者則會在 defer 函數真正調用時根據整個上下文確
定參數當前的值。
defer 后面的函數在執行的時候,函數調用的參數會被保存起來,也就是復制了一份。真正執
行的時候,實際上用到的是這個復制的變量,因此如果此變量是一個“值”,那么就和定義的時候
是一致的。如果此變量是一個“引用”,那就可能和定義的時候不一致。
舉個例子:
func main() {var whatever [3]struct{}for i := range whatever {defer func() {fmt.Println(i)}()}
}
執行結果:
2
1
0
????????defer 后面跟的是一個閉包(后面小節會講到),i 是“引用”類型的變量,for 循環結束后 i的值為 2,因此最后打印了2 1 0。
有了上面的基礎,再來看一個例子:
type number intfunc (n number) print() { fmt.Println(n) }
func (n *number) pprint() { fmt.Println(*n) }
func main() {var n numberdefer n.print()defer n.pprint()defer func() { n.print() }()defer func() { n.pprint() }()n = 3
}
執行結果:
3
3
3
0
????????注意,defer 語句的執行順序和定義的順序相反。
第四個 defer 語句是閉包,引用外部函數的 n,最終結果是 3;第三個 defer 語句同上,也是閉包;第二個 defer 語句,n 是引用,最終求值是 3; 第一個 defer 語句,對 n 直接求值,開始的時候 n=0,所以最后是 0。
我們再來看兩個延伸情況。例如,下面的例子中,return 之后的 defer 語句會執行嗎?
func main() {defer func() {fmt.Println("before return")}()if true {fmt.Println("during return")return}defer func() {fmt.Println("after return")}()
}
運行結果:?
during return
before return
????????解析:return 之后的 defer 函數不能被注冊,因此不能打印出 after return。
????????第二個延伸示例則可以視為對defer 的原理的利用。某些情況下,會故意用到 defer 的“先求
值,再延遲調用”的性質。想象這樣的場景:在一個函數里,需要打開兩個文件進行合并操作,合
并完成后,在函數結束前關閉打開的文件句柄。??
func mergeFile() error {// 打開文件一f, _ := os.Open("file1.txt")if f != nil {defer func(f io.Closer) {if err := f.Close(); err != nil {fmt.Printf("defer close file1.txt err %v\n", err)}}(f)}// 打開文件二f, _ = os.Open("file2.txt")if f != nil {defer func(f io.Closer) {if err := f.Close(); err != nil {fmt.Printf("defer close file2.txt err %v\n", err)}}(f)}// ……return nil
}
????????上面的代碼中就用到了 defer 的原理,defer 函數定義的時候,參數就已經復制進去了,之
后,真正執行 close() 函數的時候就剛好關閉的是正確的“文件”了,很巧妙。如果不這樣,將 f
當成函數參數傳遞進去的話,最后兩個語句關閉的就是同一個文件了:都是最后一個打開的文件。
在調用 close() 函數的時候,要注意一點:先判斷調用主體是否為空,否則可能會解引用了一
個空指針,進而 panic。?
?如何拆解延遲語句
????????如果 defer 像前面介紹的那樣簡單,這個世界就完美了。但事情總是沒這么簡單,defer 用得
不好,會陷入泥潭。
避免陷入泥潭的關鍵是必須深刻理解下面這條語句:
return xxx
????????上面這條語句經過編譯之后,實際上生成了三條指令:
??????1)設置返回值 = xxx。
2)調用 defer 函數。
3)空的 return。講返回值返回
第 1 和第3 步是 return 語句生成的指令,也就是說return 并不是一條原子指令;第 2 步是
defer 定義的語句,這里可能會操作返回值,從而影響最終結果。
下面來看兩個例子,試著將 return 語句和 defer 語句拆解到正確的順序。
第一個例子:?
func f() (res int) {t := 5defer func() {t = t + 5}()return t
}
拆解后:
func f() (res int) {t := 5// 1. 賦值指令res = t// 2. defer 被插入到賦值與返回之間執行,這個例子中返回值 res 沒被修改過func() {t = t + 5}// 3. 空的return 指令return
}
????????這里第二步實際上并沒有操作返回值 r,因此,main 函數中調用 f() 得到 5。?
????????第二個例子:
func f() (res int) {defer func(res int) {res = res + 5}(res)return 1
}
????????拆解后:
func f() (res int) {// 1. 賦值res = 1// 2. 這里改的 res 是之前傳進去的 res,不會改變要返回的那個 res值func(res int) {res = res + 5}(res)// 3. 空的returnreturn
}
????????第二步,改變的是傳值進去的 r,是形參的一個復制值,不會影響實參 r。因此,main 函數中
需要調用f()得到1。?
? ? ? ? 第三個例子:
package mainimport "fmt"func main() {res := deferRun()fmt.Println(res)
}func deferRun() (res int) {num := 1 defer func() {res++}() return num
}
運行結果:
2
????????在本例中,第一步是將result
的值設置為num
,此時還未執行defer
,num
的值是1
,所以result
被設置為1
,然后再執行defer
語句將result+1
,最終將result
返回,所以會打印出?2
。
????????如果把defer中的res++改成num++
func deferRun() (res int) {num := 1defer func() {num++}() return num
}
運行結果:?
1
????????第一步是將result
的值設置為num
,此時還未執行defer
,num
的值是1
,所以result
被設置為1
,然后再執行defer 即num+1
,要返回的result
并沒有變,
最終將result
返回,所以會打印出?1
。?
如何確定延遲語句的參數
????????defer 語句表達式的值在定義時就已經確定了。下面通過三個不同的函數來理解:
func f1() {var err errordefer fmt.Println(err)err = errors.New("defer1 error")return
}
func f2() {var err errordefer func() {fmt.Println(err)}()err = errors.New("defer2 error")return
}
func f3() {var err errordefer func(err error) {fmt.Println(err)}(err)err = errors.New("defer3 error")return
}
func main() {f1()f2()f3()
}
????????運行結果:?
<nil>
defer2 error
<nil>
????????第 1 和第3 個函數中,因為作為參數,err 在函數定義的時候就會求值,并且定義的時候 err
的值都是 nil,所以最后打印的結果都是 nil;第 2 個函數的參數其實也會在定義的時候求值,但
第 2 個例子中是一個閉包,它引用的變量 err 在執行的時候值最終變成 defer2 error 了。
func deferrun3() {num := 1defer func() {fmt.Println(num)}()num++return
}
運行結果:?原理同上述第二個例子,也是閉包
2
????????現實中第 3 個函數比較容易犯錯誤,在生產環境中,很容易寫出這樣的錯誤代碼,導致最后
defer 語句沒有起到作用,造成一些線上事故,要特別注意。
閉包是什么
閉包是由函數及其相關引用環境組合而成的實體,即:閉包=函數+引用環境。
一般的函數都有函數名,而匿名函數沒有。匿名函數不能獨立存在,但可以直接調用或者賦值于某個變量。匿名函數也被稱為閉包,一個閉包繼承了函數聲明時的作用域。在 Go 語言中,所有的匿名函數都是閉包。
有個不太恰當的例子:可以把閉包看成是一個類,一個閉包函數調用就是實例化一個類。閉包在運行時可以有多個實例,它會將同一個作用域里的變量和常量捕獲下來,無論閉包在什么地方被調用(實例化)時,都可以使用這些變量和常量。而且,閉包捕獲的變量和常量是引用傳遞,不是值傳遞。
舉個簡單的例子:
func main() {var a = Accumulator()fmt.Printf("%d\n", a(1))fmt.Printf("%d\n", a(10))fmt.Printf("%d\n", a(100))fmt.Println("------------------------")var b = Accumulator()fmt.Printf("%d\n", b(1))fmt.Printf("%d\n", b(10))fmt.Printf("%d\n", b(100))
}
func Accumulator() func(int) int {var x intreturn func(delta int) int {fmt.Printf("(%+v, %+v) - ", &x, x)x += deltareturn x}
}
執行結果是:?
(0xc420014070, 0) - 1
(0xc420014070, 1) - 11
(0xc420014070, 11) - 111
------------------------
(0xc4200140b8, 0) - 1
(0xc4200140b8, 1) - 11
(0xc4200140b8, 11) – 111
????????閉包引用了 x 變量,a,b 可看作 2 個不同的實例,實例之間互不影響。實例內部,x 變量
是同一個地址,因此具有“累加效應”。?
延遲語句如何配合恢復語句
????????Go 語言被詬病多次的就是它的 error,實際項目里經常出現各種 error 滿天飛,正常的代碼邏
輯里有很多 error 處理的代碼塊。函數總是會返回一個 error,留給調用者處理;而如果是致命的錯
誤,比如程序執行初始化的時候出問題,最好直接 panic 掉,避免上線運行后出更大的問題。
有些時候,需要從異常中恢復。比如服務器程序遇到嚴重問題,產生了 panic,這時至少可以
在程序崩潰前做一些“掃尾工作”,比如關閉客戶端的連接,防止客戶端一直等待等;并且單個請求導致的 panic,也不應該影響整個服務器程序的運行。
recover異常捕獲
????????異常其實就是指程序運行過程中發生了panic
,那么我們為了不讓程序報錯退出,可以在程序中加入recover
機制,將異常捕獲,打印出異常,這樣也方便我們定位錯誤。而捕獲的方式我們之前在講defer
的時候也提到過,一般是用recover
和defer
搭配使用來捕獲異常。
下面請看個具體例子:
func main() { defer func() { if error:=recover();error!=nil{ fmt.Println("出現了panic,使用reover獲取信息:",error) } }() fmt.Println("11111111111") panic("出現panic") fmt.Println("22222222222") }
運行結果:
11111111111
出現了panic,使用reover獲取信息: 出現panic
????????注意,這里有了recover
之后,程序不會在panic
出中斷,再執行完panic
之后,會接下來執行defer recover
函數,但是當前函數panic
后面的代碼不會被執行,但是調用該函數的代碼會接著執行。
如果我們在main
函數中未加入defer func(){...}
,當我們的程序運行到底8行時就會panic
掉,而通常在我們的業務程序中對于程序panic
是不可容忍的,我們需要程序健壯的運行,而不是是不是因為一些panic
掛掉又被拉起,所以當發生panic
的時候我們要讓程序能夠繼續運行,并且獲取到發生panic
的具體錯誤,這就可以用上述方法。
panic傳遞
????????當一個函數發生了panic
之后,若在當前函數中沒有recover
,會一直向外層傳遞直到主函數,如果遲遲沒有recover
的話,那么程序將終止。如果在過程中遇到了最近的recover
,則將被捕獲。
看下面例子:
package mainimport "fmt"func testPanic1(){fmt.Println("testPanic1上半部分")testPanic2()fmt.Println("testPanic1下半部分")
}func testPanic2(){defer func() {recover()}()fmt.Println("testPanic2上半部分")testPanic3()fmt.Println("testPanic2下半部分")
}func testPanic3(){fmt.Println("testPanic3上半部分")panic("在testPanic3出現了panic")fmt.Println("testPanic3下半部分")
}func main() {fmt.Println("程序開始")testPanic1()fmt.Println("程序結束")
}
運行結果:
程序開始
testPanic1上半部分
testPanic2上半部分
testPanic3上半部分
testPanic1下半部分
程序結束
解析:
調用鏈:main-->testPanic1-->testPanic2-->testPanic3
,但是在testPanic3
中發現了一個panic
,由于testPanic3
沒有recover
,向上找,在testPanic2
中找到了recover
,panic
被捕獲了,程序接著運行,由于testPanic3
發生了panic
,所以不再繼續運行,函數跳出返回到testPanic2
,testPanic2
中捕獲到了panic
,也不會再繼續執行,跳出函數testPanic2
,到了testPanic1
接著運行。
????????所以recover
和panic
可以總結為以下兩點:
????????這里的調用鏈指的是同一個函數中(如果panic是在另外一個go程中,是捕獲不到的。即一個go程是無法捕獲到另一個go程中的panic)
recover()
只能恢復當前函數級或以當前函數為首的調用鏈中的函數中的panic()
,恢復后調用當前函數結束,但是調用此函數的函數繼續執行- 函數發生了
panic
之后會一直向上傳遞,如果直至main
函數都沒有recover()
,程序將終止,如果是碰見了recover()
,將被recover
捕獲。
defer...recover
? ? ? ? panic 會停掉當前正在執行的程序,而不只是當前線程。在這之前,它會有序地執行完當前線
程 defer 列表里的語句,其他協程里定義的 defer 語句不作保證。所以在 defer 里定義一個recover 語句,防止程序直接掛掉,就可以起到類似 Java 里 try...catch 的效果。
注意,recover() 函數只在 defer 的函數中直接調用才有效。例如:
func main() {defer fmt.Println("defer main")var user = os.Getenv("USER_")go func() {defer func() {fmt.Println("defer caller")if err := recover(); err != nil {fmt.Println("recover success. err: ", err)}}()func() {defer func() {fmt.Println("defer here")}()if user == "" {panic("should set user env.")}// 此處不會執行fmt.Println("after panic")}()}()time.Sleep(100)fmt.Println("end of main function")
}
程序的執行結果:?
defer here
defer caller
recover success. err: should set user env.
end of main function
defer main
????????代碼中的 panic 最終會被 recover 捕獲到。這樣的處理方式在一個 http server 的主流程常常
會被用到。一次偶然的請求可能會觸發某個 bug,這時用 recover 捕獲 panic,穩住主流程,不影
響其他請求。
同樣,我們再來看幾個延伸示例。這些例子都與 recover() 函數的調用位置有關。
考慮以下寫法,程序是否能正確 recover 嗎?如果不能,原因是什么:?
func main() {defer f()panic(404)
}
func f() {if e := recover(); e != nil {fmt.Println("recover")return}
}
????????能。在 defer 的函數中調用,生效。?
func main() {recover()panic(404)
}
????????不能。直接調用 recover,返回 nil。?
func main() {defer recover()panic(404)
}
????????不能。要在 defer 函數里調用 recover。?
func main() {defer func() {if e := recover(); e != nil {fmt.Println("recover")}}()panic(404)
}
????????能。在 defer 的函數中調用,生效。?
func main() {defer func() {recover()}()panic(404)
}
????????能。在 defer 的函數中調用,生效。?
func main() {defer func() {defer func() {recover()}()}()panic(404)
}
????????不能。多重 defer 嵌套。?
為什么無法從父goroutine 恢復子goroutine 的panic
????????對于這個問題,其實更普遍問題是:為什么無法 recover 其他 goroutine 里產生的 panic??
????????為什么不能從父 goroutine 中恢復子 goroutine 的 panic?或者一般地說,為什么某個
goroutine 不能捕獲其他 goroutine 內產生的 panic?
????????是設計使然:因為goroutine 被設計為一個獨立的代碼執行單元,擁有自己的執行棧,不與其他 goroutine 共享任何數據。這意味著,無法讓 goroutine 擁有返回值、也無法讓 goroutine 擁有自身的 ID 編號等。若需要與其他 goroutine 產生交互,要么可以使用 channel 的方式與其他 goroutine 進行通信,要么通過共享內存同步方式對共享的內存添加讀寫鎖。