Golang defer延遲語(yǔ)句的實(shí)現(xiàn)
一、defer的簡(jiǎn)單使用
defer
擁有注冊(cè)延遲調(diào)用的機(jī)制,defer
關(guān)鍵字后面跟隨的語(yǔ)句或者函數(shù),會(huì)在當(dāng)前的函數(shù)return
正常結(jié)束 或者 panic
異常結(jié)束 后執(zhí)行。
但是defer
只有在注冊(cè)后,最后才能生效調(diào)用執(zhí)行,return
之后的defer
語(yǔ)句是不會(huì)執(zhí)行的,因?yàn)椴](méi)有注冊(cè)成功。
如下例子:
func main() { defer func() { fmt.Println(111) }() fmt.Println(222) return defer func() { fmt.Println(333) }() }
執(zhí)行結(jié)果:
222
111
解析:222
、111
是在return
之前注冊(cè)的,所以如期執(zhí)行,333
是在return
之后注冊(cè)的,注冊(cè)失敗,執(zhí)行不了。
defer
在需要資源釋放的場(chǎng)景非常有用,可以很方便地在函數(shù)結(jié)束前執(zhí)行一些操作。
比如在 打開(kāi)連接/關(guān)閉連接 、加鎖/釋放鎖、打開(kāi)文件/關(guān)閉文件 這些場(chǎng)景下:
file, err := os.Open("1.txt") if err != nil { panic(err) } if file != nil { defer file.Close() }
這里要注意的是:在調(diào)用file.Close()
之前,需要判斷file
是否為空,避免出現(xiàn)異常情況。
再來(lái)看一個(gè)錯(cuò)誤示范,沒(méi)有正確使用defer
的例子:
player.mu.Lock() rand.Intn(number) player.mu.Unlock()
這三行代碼,存在兩個(gè)問(wèn)題:
1. 中間這行代碼 rand.Intn(number)
是有可能發(fā)生panic
的,這就會(huì)導(dǎo)致沒(méi)有正常解鎖。
2. 這樣的代碼在項(xiàng)目中后續(xù)可能被其他人修改,在rand.Intn(number)
后增加更多的邏輯,這是完全不可控的。
在Lock
和 Unlock
之間的代碼一旦出現(xiàn) panic
,就會(huì)造成死鎖。因此,即使邏輯非常簡(jiǎn)單,使用defer
也是很有必要的,因?yàn)樾枨罂傇谧兓?,代碼也總會(huì)被修改。
二、defer的函數(shù)參數(shù)與閉包引用
defer
延遲語(yǔ)句不會(huì)馬上執(zhí)行,而是會(huì)進(jìn)入一個(gè)棧,函數(shù)return
前,會(huì)按先進(jìn)后出的順序執(zhí)行。
先進(jìn)后出的原因是后面定義的函數(shù)可能會(huì)依賴(lài)前面的資源,自然要先執(zhí)行;否則,如果前面的先執(zhí)行了,那么后面函數(shù)的依賴(lài)就沒(méi)有了,就可能會(huì)導(dǎo)致出錯(cuò)。
在defer
函數(shù)定義時(shí),對(duì)外部變量的引用有三種方式:值傳參、指針傳參、閉包引用。
- 值傳參:在
defer
定義時(shí)就把值傳遞給defer
,并復(fù)制一份cache起來(lái),defer調(diào)用時(shí)和定義的時(shí)候值是一致的。 - 指針傳參:在
defer
定義時(shí)就把指針傳遞給defer
,defer調(diào)用時(shí)根據(jù)整個(gè)上下文確定參數(shù)當(dāng)前的值。 - 閉包引用:在
defer
定義時(shí)就把值引用傳遞給defer
,defer調(diào)用時(shí)根據(jù)整個(gè)上下文確定參數(shù)當(dāng)前的值。
下面通過(guò)例子加深一下理解。
例子1:
func main() { var arr [4]struct{} for i := range arr { defer func() { fmt.Println(i) }() } }
執(zhí)行結(jié)果:
3
3
3
3
解析:因?yàn)?code>defer 后面跟著的是一個(gè)閉包,根據(jù)整個(gè)上下文確定,for
循環(huán)結(jié)束后i
的值為3,因此最后打印了4個(gè)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í)行順序和定義的順序是相反的;
第三個(gè)defer
語(yǔ)句是閉包,引用的外部變量n
,defer調(diào)用時(shí)根據(jù)上下文確定,最終結(jié)果是4;
第二個(gè)defer
語(yǔ)句是指針傳參,defer調(diào)用時(shí)根據(jù)整個(gè)上下文確定參數(shù)當(dāng)前的值,最終結(jié)果是4;
第一個(gè)defer
語(yǔ)句是值傳參,defer調(diào)用時(shí)和定義的時(shí)候值是一致的,最終結(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
解析:先說(shuō)結(jié)論,這個(gè)例子的代碼沒(méi)有問(wèn)題,兩個(gè)文件都會(huì)被成功關(guān)閉。這個(gè)是對(duì)defer
原理的應(yīng)用,因?yàn)?code>defer 函數(shù)在定義的時(shí)候,參數(shù)就已經(jīng)復(fù)制進(jìn)去了,這里是值傳參,真正執(zhí)行close()
函數(shù)的時(shí)候就剛好關(guān)閉的是正確的文件。如果不把f
當(dāng)做值傳參,最后兩個(gè)close()
函數(shù)關(guān)閉的就是同一個(gè)文件了,都是最后打開(kāi)的那個(gè)文件。
例子3的錯(cuò)誤示范:
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
解析:
第一個(gè)和第三個(gè)函數(shù)中,都是作為參數(shù),進(jìn)行值傳參,err
在定義的時(shí)候就會(huì)求值,因?yàn)槎x的時(shí)候值都是nil
,所以最后的結(jié)果都是nil
;
第二個(gè)函數(shù)的參數(shù)在定義的時(shí)候也求值了,但是它是個(gè)閉包,查看上下文發(fā)現(xiàn)最后值被修改為func2 error
;
第四個(gè)函數(shù)是指針傳參,最后值被修改為func4 error
;
現(xiàn)實(shí)中,第三個(gè)函數(shù)閉包的例子是比較容易犯的錯(cuò)誤,導(dǎo)致最后defer
語(yǔ)句沒(méi)有起到作用,造成生產(chǎn)上的事故,需要特別注意。
三、defer的語(yǔ)句拆解
從返回值出發(fā)來(lái)拆解延遲語(yǔ)句 defer
。
? return xxx
這條語(yǔ)句經(jīng)過(guò)編譯之后,實(shí)際上生成了三條指令:
1. 返回值 = xxx
2. 調(diào)用 defer 函數(shù)
3. 空的 return
其中,1
和 3
是return
語(yǔ)句生成的指令,2
是defer
語(yǔ)句生成的指令??梢钥闯觯?/p>
return
并不是一條原子指令;defer
語(yǔ)句在第二步調(diào)用,這里可能操作返回值,從而影響最終結(jié)果。
接下來(lái)通過(guò)例子來(lái)加深理解。
例子1:
func func1() (r int) { t := 3 defer func() { t = t + 3 }() return t } func main() { r := func1() fmt.Println(r) }
執(zhí)行結(jié)果:
3
語(yǔ)句拆解:
func func1() (r int) { t := 3 // 1.返回值=xxx:賦值指令 r = t // 2.調(diào)用defer函數(shù):defer在賦值與返回之前執(zhí)行,這個(gè)例子中返回值r沒(méi)有被修改過(guò) func() { t = t + 3 }() // 3.空的return return } func main() { r := func1() fmt.Println(r) }
解析:因?yàn)榈诙€(gè)步驟里并沒(méi)有操作返回值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
語(yǔ)句拆解:
func func2() (r int) { // 1.返回值=xxx:賦值指令 r = 1 // 2.調(diào)用defer函數(shù):因?yàn)槭侵祩鲄?,所以修改的r是個(gè)復(fù)制的值,不會(huì)影響要返回的那個(gè)r值。 func(r int) { r = r + 3 }(r) // 3.空的return return } func main() { r := func2() fmt.Println(r) }
解析:因?yàn)榈诙€(gè)步驟里改變的是傳值進(jìn)去的r
值,是一個(gè)形參的復(fù)制值,不會(huì)影響實(shí)參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
語(yǔ)句拆解:
func func3() (r int) { // 1.返回值=xxx:賦值指令 r = 1 // 2.調(diào)用defer函數(shù):因?yàn)槭情]包,捕獲的變量是引用傳遞,所以會(huì)修改返回的那個(gè)r值。 func() { r = r + 3 }() // 3.空的return return } func main() { r := func3() fmt.Println(r) }
解析:因?yàn)榈诙€(gè)步驟里改變的r
值是閉包,閉包中捕獲的變量是引用傳遞,不是值傳遞,所以最終得到的結(jié)果是4
。
四、defer中的recover
代碼中的panic
最終會(huì)被recover
捕獲到。在日常開(kāi)發(fā)中,可能某一條協(xié)議的邏輯觸發(fā)了某一個(gè)bug
造成panic
,這時(shí)就可以用recover
去捕獲panic
,穩(wěn)住主流程,不影響其他協(xié)議的業(yè)務(wù)邏輯。
需要注意的是,recover
函數(shù)只在defer
的函數(shù)中直接調(diào)用才生效。
通過(guò)例子看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
,因?yàn)樵?code>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
解析:錯(cuò)誤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
解析:錯(cuò)誤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
解析:錯(cuò)誤recover
,不能在多重defer
嵌套里調(diào)用recover
。
另外需要注意的一點(diǎn)是,父goroutine
無(wú)法 recover
住 子goroutine
的 panic
。
原因是,goroutine
被設(shè)計(jì)為一個(gè)獨(dú)立的代碼執(zhí)行單元,擁有自己的執(zhí)行棧,不與其他goroutine
共享任何的數(shù)據(jù)。
也就是說(shuō),無(wú)法讓goroutine
擁有返回值,也無(wú)法讓goroutine
擁有自身的ID
編號(hào)。
如果希望有一個(gè)全局的panic
捕獲中心,那么可以通過(guò)channel
來(lái)實(shí)現(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ì)上是對(duì)go
關(guān)鍵字進(jìn)行了一層封裝,確保在執(zhí)行并發(fā)單元前插入一個(gè)defer
,從而保證能夠recover
住panic
。但是這個(gè)方案并不完美,如果開(kāi)發(fā)人員不使用GoSafe
函數(shù)來(lái)創(chuàng)建goroutine
,而是自己創(chuàng)建,并且在代碼中出現(xiàn)了panic
,那么仍然會(huì)造成程序崩潰。
到此這篇關(guān)于Golang defer延遲語(yǔ)句的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Golang defer延遲語(yǔ)句內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go?常見(jiàn)設(shè)計(jì)模式之單例模式詳解
單例模式是設(shè)計(jì)模式中最簡(jiǎn)單的一種模式,單例模式能夠確保無(wú)論對(duì)象被實(shí)例化多少次,全局都只有一個(gè)實(shí)例存在,在Go?語(yǔ)言有多種方式可以實(shí)現(xiàn)單例模式,所以我們今天就來(lái)一起學(xué)習(xí)下吧2023-07-07Go語(yǔ)言結(jié)合正則表達(dá)式實(shí)現(xiàn)高效獲取數(shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言如何結(jié)合正則表達(dá)式實(shí)現(xiàn)高效獲取數(shù)據(jù),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-04-04使用Go基于WebSocket構(gòu)建千萬(wàn)級(jí)視頻直播彈幕系統(tǒng)的代碼詳解
這篇文章主要介紹了使用Go基于WebSocket構(gòu)建千萬(wàn)級(jí)視頻直播彈幕系統(tǒng),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07Go?WEB框架使用攔截器驗(yàn)證用戶(hù)登錄狀態(tài)實(shí)現(xiàn)
這篇文章主要為大家介紹了Go?WEB框架使用攔截器驗(yàn)證用戶(hù)登錄狀態(tài)實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07Go語(yǔ)言學(xué)習(xí)之context包的用法詳解
日常Go開(kāi)發(fā)中,Context包是用的最多的一個(gè)了,幾乎所有函數(shù)的第一個(gè)參數(shù)都是ctx,那么我們?yōu)槭裁匆獋鬟fContext呢,Context又有哪些用法,底層實(shí)現(xiàn)是如何呢?相信你也一定會(huì)有探索的欲望,那么就跟著本篇文章,一起來(lái)學(xué)習(xí)吧2022-10-10Go+Redis實(shí)現(xiàn)常見(jiàn)限流算法的示例代碼
限流是項(xiàng)目中經(jīng)常需要使用到的一種工具,一般用于限制用戶(hù)的請(qǐng)求的頻率,也可以避免瞬間流量過(guò)大導(dǎo)致系統(tǒng)崩潰,或者穩(wěn)定消息處理速率。這篇文章主要是使用Go+Redis實(shí)現(xiàn)常見(jiàn)的限流算法,需要的可以參考一下2023-04-04golang新手不注意可能會(huì)出現(xiàn)的一些小問(wèn)題
最近在學(xué)習(xí)golang,發(fā)現(xiàn)了一些新手們需要注意的小問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于golang新手不注意可能會(huì)出現(xiàn)的一些小問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-12-12