Golang defer延遲語句的實現(xiàn)
一、defer的簡單使用
defer 擁有注冊延遲調(diào)用的機制,defer 關(guān)鍵字后面跟隨的語句或者函數(shù),會在當(dāng)前的函數(shù)return 正常結(jié)束 或者 panic 異常結(jié)束 后執(zhí)行。
但是defer 只有在注冊后,最后才能生效調(diào)用執(zhí)行,return 之后的defer 語句是不會執(zhí)行的,因為并沒有注冊成功。
如下例子:
func main() {
defer func() {
fmt.Println(111)
}()
fmt.Println(222)
return
defer func() {
fmt.Println(333)
}()
}
執(zhí)行結(jié)果:
222
111
解析:222 、111 是在return 之前注冊的,所以如期執(zhí)行,333 是在return 之后注冊的,注冊失敗,執(zhí)行不了。
defer 在需要資源釋放的場景非常有用,可以很方便地在函數(shù)結(jié)束前執(zhí)行一些操作。
比如在 打開連接/關(guān)閉連接 、加鎖/釋放鎖、打開文件/關(guān)閉文件 這些場景下:
file, err := os.Open("1.txt")
if err != nil {
panic(err)
}
if file != nil {
defer file.Close()
}
這里要注意的是:在調(diào)用file.Close() 之前,需要判斷file 是否為空,避免出現(xiàn)異常情況。
再來看一個錯誤示范,沒有正確使用defer 的例子:
player.mu.Lock() rand.Intn(number) player.mu.Unlock()
這三行代碼,存在兩個問題:
1. 中間這行代碼 rand.Intn(number) 是有可能發(fā)生panic 的,這就會導(dǎo)致沒有正常解鎖。
2. 這樣的代碼在項目中后續(xù)可能被其他人修改,在rand.Intn(number) 后增加更多的邏輯,這是完全不可控的。
在Lock 和 Unlock 之間的代碼一旦出現(xiàn) panic ,就會造成死鎖。因此,即使邏輯非常簡單,使用defer 也是很有必要的,因為需求總在變化,代碼也總會被修改。
二、defer的函數(shù)參數(shù)與閉包引用
defer 延遲語句不會馬上執(zhí)行,而是會進入一個棧,函數(shù)return 前,會按先進后出的順序執(zhí)行。
先進后出的原因是后面定義的函數(shù)可能會依賴前面的資源,自然要先執(zhí)行;否則,如果前面的先執(zhí)行了,那么后面函數(shù)的依賴就沒有了,就可能會導(dǎo)致出錯。
在defer 函數(shù)定義時,對外部變量的引用有三種方式:值傳參、指針傳參、閉包引用。
- 值傳參:在
defer定義時就把值傳遞給defer,并復(fù)制一份cache起來,defer調(diào)用時和定義的時候值是一致的。 - 指針傳參:在
defer定義時就把指針傳遞給defer,defer調(diào)用時根據(jù)整個上下文確定參數(shù)當(dāng)前的值。 - 閉包引用:在
defer定義時就把值引用傳遞給defer,defer調(diào)用時根據(jù)整個上下文確定參數(shù)當(dāng)前的值。
下面通過例子加深一下理解。
例子1:
func main() {
var arr [4]struct{}
for i := range arr {
defer func() {
fmt.Println(i)
}()
}
}
執(zhí)行結(jié)果:
3
3
3
3
解析:因為defer 后面跟著的是一個閉包,根據(jù)整個上下文確定,for 循環(huán)結(jié)束后i 的值為3,因此最后打印了4個3。
例子2:
func main() {
var n int
// 值傳參
defer func(n1 int) {
fmt.Println(n1)
}(n)
// 指針傳參
defer func(n2 *int) {
fmt.Println(*n2)
}(&n)
// 閉包
defer func() {
fmt.Println(n)
}()
n = 4
}
執(zhí)行結(jié)果:
4
4
0
解析:
defer 執(zhí)行順序和定義的順序是相反的;
第三個defer 語句是閉包,引用的外部變量n ,defer調(diào)用時根據(jù)上下文確定,最終結(jié)果是4;
第二個defer 語句是指針傳參,defer調(diào)用時根據(jù)整個上下文確定參數(shù)當(dāng)前的值,最終結(jié)果是4;
第一個defer 語句是值傳參,defer調(diào)用時和定義的時候值是一致的,最終結(jié)果是0;
例子3:
func main() {
// 文件1
f, _ := os.Open("1.txt")
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close file err 1 %v\n", err)
}
}(f)
}
// 文件2
f, _ = os.Open("2.txt")
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close file err 2 %v\n", err)
}
}(f)
}
fmt.Println("success")
}
執(zhí)行結(jié)果:
success
解析:先說結(jié)論,這個例子的代碼沒有問題,兩個文件都會被成功關(guān)閉。這個是對defer 原理的應(yīng)用,因為defer 函數(shù)在定義的時候,參數(shù)就已經(jīng)復(fù)制進去了,這里是值傳參,真正執(zhí)行close() 函數(shù)的時候就剛好關(guān)閉的是正確的文件。如果不把f 當(dāng)做值傳參,最后兩個close() 函數(shù)關(guān)閉的就是同一個文件了,都是最后打開的那個文件。
例子3的錯誤示范:
func main() {
// 文件1
f, _ := os.Open("1.txt")
if f != nil {
defer func() {
if err := f.Close(); err != nil {
fmt.Printf("defer close file err 1 %v\n", err)
}
}()
}
// 文件2
f, _ = os.Open("2.txt")
if f != nil {
defer func() {
if err := f.Close(); err != nil {
fmt.Printf("defer close file err 2 %v\n", err)
}
}()
}
fmt.Println("success")
}
執(zhí)行結(jié)果:
success
defer close file err 1 close 2.txt: file already closed
例子4:
// 值傳參
func func1() {
var err error
defer fmt.Println(err)
err = errors.New("func1 error")
return
}
// 閉包
func func2() {
var err error
defer func() {
fmt.Println(err)
}()
err = errors.New("func2 error")
return
}
// 值傳參
func func3() {
var err error
defer func(err error) {
fmt.Println(err)
}(err)
err = errors.New("func3 error")
return
}
// 指針傳參
func func4() {
var err error
defer func(err *error) {
fmt.Println(*err)
}(&err)
err = errors.New("func4 error")
return
}
func main() {
func1()
func2()
func3()
func4()
}
執(zhí)行結(jié)果:
<nil>
func2 error
<nil>
func4 error
解析:
第一個和第三個函數(shù)中,都是作為參數(shù),進行值傳參,err 在定義的時候就會求值,因為定義的時候值都是nil ,所以最后的結(jié)果都是nil ;
第二個函數(shù)的參數(shù)在定義的時候也求值了,但是它是個閉包,查看上下文發(fā)現(xiàn)最后值被修改為func2 error ;
第四個函數(shù)是指針傳參,最后值被修改為func4 error ;
現(xiàn)實中,第三個函數(shù)閉包的例子是比較容易犯的錯誤,導(dǎo)致最后defer 語句沒有起到作用,造成生產(chǎn)上的事故,需要特別注意。
三、defer的語句拆解
從返回值出發(fā)來拆解延遲語句 defer 。
? return xxx
這條語句經(jīng)過編譯之后,實際上生成了三條指令:
1. 返回值 = xxx
2. 調(diào)用 defer 函數(shù)
3. 空的 return
其中,1 和 3 是return 語句生成的指令,2 是defer 語句生成的指令??梢钥闯觯?/p>
return 并不是一條原子指令;defer 語句在第二步調(diào)用,這里可能操作返回值,從而影響最終結(jié)果。
接下來通過例子來加深理解。
例子1:
func func1() (r int) {
t := 3
defer func() {
t = t + 3
}()
return t
}
func main() {
r := func1()
fmt.Println(r)
}
執(zhí)行結(jié)果:
3
語句拆解:
func func1() (r int) {
t := 3
// 1.返回值=xxx:賦值指令
r = t
// 2.調(diào)用defer函數(shù):defer在賦值與返回之前執(zhí)行,這個例子中返回值r沒有被修改過
func() {
t = t + 3
}()
// 3.空的return
return
}
func main() {
r := func1()
fmt.Println(r)
}
解析:因為第二個步驟里并沒有操作返回值r ,所以最終得到的結(jié)果是3 。
例子2:
func func2() (r int) {
defer func(r int) {
r = r + 3
}(r)
return 1
}
func main() {
r := func2()
fmt.Println(r)
}
執(zhí)行結(jié)果:
1
語句拆解:
func func2() (r int) {
// 1.返回值=xxx:賦值指令
r = 1
// 2.調(diào)用defer函數(shù):因為是值傳參,所以修改的r是個復(fù)制的值,不會影響要返回的那個r值。
func(r int) {
r = r + 3
}(r)
// 3.空的return
return
}
func main() {
r := func2()
fmt.Println(r)
}
解析:因為第二個步驟里改變的是傳值進去的r 值,是一個形參的復(fù)制值,不會影響實參r ,所以最終得到的結(jié)果是1 。
例子3:
func func3() (r int) {
defer func() {
r = r + 3
}()
return 1
}
func main() {
r := func3()
fmt.Println(r)
}
執(zhí)行結(jié)果:
4
語句拆解:
func func3() (r int) {
// 1.返回值=xxx:賦值指令
r = 1
// 2.調(diào)用defer函數(shù):因為是閉包,捕獲的變量是引用傳遞,所以會修改返回的那個r值。
func() {
r = r + 3
}()
// 3.空的return
return
}
func main() {
r := func3()
fmt.Println(r)
}
解析:因為第二個步驟里改變的r 值是閉包,閉包中捕獲的變量是引用傳遞,不是值傳遞,所以最終得到的結(jié)果是4 。
四、defer中的recover
代碼中的panic 最終會被recover 捕獲到。在日常開發(fā)中,可能某一條協(xié)議的邏輯觸發(fā)了某一個bug 造成panic ,這時就可以用recover 去捕獲panic ,穩(wěn)住主流程,不影響其他協(xié)議的業(yè)務(wù)邏輯。
需要注意的是,recover 函數(shù)只在defer 的函數(shù)中直接調(diào)用才生效。
通過例子看recover 調(diào)用情況。
例子1:
func func1() {
if err := recover(); err != nil {
fmt.Println("func1 recover", err)
return
}
}
func main() {
defer func1()
panic("func1 panic")
}
執(zhí)行結(jié)果:
func1 recover func1 panic
解析:正確recover ,因為在defer 中調(diào)用的,所以可以生效。
例子2:
func main() {
recover()
panic("func2 panic")
}
執(zhí)行結(jié)果:
panic: func2 panic
goroutine 1 [running]:
main.main()
C:/Users/ycz/go/ccc.go:5 +0x31
exit status 2
解析:錯誤recover ,直接調(diào)用recover ,返回nil 。
例子3:
func main() {
defer recover()
panic("func3 panic")
}
執(zhí)行結(jié)果:
panic: func3 panic
goroutine 1 [running]:
main.main()
C:/Users/ycz/go/ccc.go:5 +0x65
exit status 2
解析:錯誤recover ,recover 需要在defer 的函數(shù)里調(diào)用。
例子4:
func main() {
defer func() {
defer func() {
recover()
}()
}()
panic("func4 panic")
}
執(zhí)行結(jié)果:
panic: func4 panic
goroutine 1 [running]:
main.main()
C:/Users/ycz/go/ccc.go:9 +0x49
exit status 2
解析:錯誤recover ,不能在多重defer 嵌套里調(diào)用recover 。
另外需要注意的一點是,父goroutine 無法 recover 住 子goroutine 的 panic 。
原因是,goroutine 被設(shè)計為一個獨立的代碼執(zhí)行單元,擁有自己的執(zhí)行棧,不與其他goroutine 共享任何的數(shù)據(jù)。
也就是說,無法讓goroutine 擁有返回值,也無法讓goroutine 擁有自身的ID 編號。
如果希望有一個全局的panic 捕獲中心,那么可以通過channel 來實現(xiàn),如下示例:
var panicNotifyManage chan interface{}
func StartGlobalPanicRecover() {
panicNotifyManage = make(chan interface{})
go func() {
select {
case err := <-panicNotifyManage:
fmt.Println("panicNotifyManage--->", err)
}
}()
}
func GoSafe(f func()) {
go func() {
defer func() {
if err := recover(); err != nil {
panicNotifyManage <- err
}
}()
f()
}()
}
func main() {
StartGlobalPanicRecover()
f1 := func() {
panic("f1 panic")
}
GoSafe(f1)
time.Sleep(time.Second)
}
解析:GoSafe() 本質(zhì)上是對go 關(guān)鍵字進行了一層封裝,確保在執(zhí)行并發(fā)單元前插入一個defer ,從而保證能夠recover 住panic 。但是這個方案并不完美,如果開發(fā)人員不使用GoSafe 函數(shù)來創(chuàng)建goroutine ,而是自己創(chuàng)建,并且在代碼中出現(xiàn)了panic ,那么仍然會造成程序崩潰。
到此這篇關(guān)于Golang defer延遲語句的實現(xiàn)的文章就介紹到這了,更多相關(guān)Golang defer延遲語句內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go語言生成隨機數(shù)和隨機字符串的實現(xiàn)方法
隨機數(shù)在很多時候都可以用到,尤其是登錄時,本文就詳細的介紹一下go語言生成隨機數(shù)和隨機字符串的實現(xiàn)方法,具有一定的參考價值,感興趣的可以了解一下2021-12-12

