深入學(xué)習(xí)Go延遲語句
1 延遲語句是什么
編程的時候,經(jīng)常會需要申請一些資源,比如數(shù)據(jù)庫連接、文件、鎖等,這些資源需要再使用后釋放掉,否則會造成內(nèi)存泄露。但是編程人員經(jīng)常容易忘記釋放這些資源,從而造成一些事故。 Go 語言直接在語言層面提供 defer 關(guān)鍵字,在申請資源語句的下一行,可以直接用 defer 語句來注冊函數(shù)結(jié)束后執(zhí)行釋放資源的操作。因為這樣一顆小小的語法糖,忘關(guān)閉資源語句的情況就打打地減少了。
defer 是 Go 語言提供的一種用于注冊延遲調(diào)用的機制:讓函數(shù)或語句可以在當前函數(shù)執(zhí)行完畢后(包括通過 return 正常結(jié)束或者 panic 導(dǎo)致的異常結(jié)束)執(zhí)行。在需要釋放資源的場景非常有用,可以很方便在函數(shù)結(jié)束前做一些清理操作。在打開資源語句的下一行,直接使用 defer 就可以在函數(shù)返回前釋放資源,可謂相當?shù)母咝А?/p>
defer 通常用于一些成對操作的場景:打開連接 / 關(guān)閉連接、加鎖 / 釋放鎖、打開文件 / 關(guān)閉文件等。使用非常簡單:
f, err := os.Open(filename) if err != nil { panic(err) } if f != nil { defer f.Close() }
在打開文件的語句附近,用 defer 語句關(guān)閉文件。這樣,在函數(shù)結(jié)束之前,會自動執(zhí)行 defer 后面的語句來關(guān)閉文件。注意,要先判斷 f 是否為空,如果 f 不為空,在調(diào)用 f.Close() 函數(shù),避免出現(xiàn)異常情況。
當然, defer 會有短暫延遲,對時間要求特別高的程序,可以避免使用它,其他情況一般可以忽略它帶來的延遲。特別是 Go 1.14 又對 defer 做了很大幅度的優(yōu)化,效率提升不少。
下面是一個反面例子:
r.mu.Lock() rand.Intn(param) r.mu.Unlock()
上面只有三行代碼,看起來這里不用 defer 執(zhí)行 Unlock 并沒有什么問題。其實并不是這樣,中間這行代碼 rand.Intn(param) 其實是有可能發(fā)生 panic 的,更嚴重的情況是,這段代碼很有可能被其他人修改,增加更多的邏輯,而這完全不可控。也就是說,在 Lock 和 Unlock 之間的代碼一旦出現(xiàn)異常情況導(dǎo)致 panic ,就會形成死鎖。因此這里的邏輯是,即使看起來非常簡單的代碼,使用 defer 也是有必要的,因為需求總是在變化,代碼也總會被修改。
2 延遲語句的執(zhí)行順序是什么
先看下官方文檔對 defer 的解釋:
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usual and saved anew but the actual function is not invoked. Instead, deferred functions are invoked immediately before the surrounding function returns, in the reverse order they were deferred. If a deferred function value evaluates to nil, execution panics when the function is invoked, not when the “defer” statement is executed.(每次 defer 語句執(zhí)行的時候,會把函數(shù) “壓棧”,函數(shù)參數(shù)會被復(fù)制下來;當外層函數(shù) (注意不是代碼塊,如一個 for 循環(huán)塊并不是外層函數(shù)) 退出時,defer 函數(shù)按照定義 的順序逆序執(zhí)行;如果 defer 執(zhí)行的函數(shù)為 nil,那么會在最終調(diào)用函數(shù)的時候產(chǎn)生 panic。)
defer 語句并不會馬上執(zhí)行,而是會進入一個棧,函數(shù) return 前,會按照先進后出的順序執(zhí)行。也就是一說,最先被定義的 defer 語句最后執(zhí)行。先進后出的原因是后面定義的函數(shù)可能會依賴前面的資源,自然要先執(zhí)行;否則,如果前面先執(zhí)行了,那后面函數(shù)的依賴就沒有了,因而可能會出錯。
在 defer 函數(shù)定義時,對外部變量的引用有兩種方式:函數(shù)參數(shù)、閉包引用。前者再 defer 定義時就把值傳遞給 defer,并被 cache 起來;后者則會在 defer 函數(shù)真正調(diào)用時根據(jù)整個上下文確定參數(shù)當前的值。
defer 后面的函數(shù)在執(zhí)行的時候,函數(shù)調(diào)用的參數(shù)會被保存起來,也就是復(fù)制一份。真正執(zhí)行的時候,實際上用到的是這個復(fù)制的變量,因此如果此變量是一個” 值 “,那么就和定義的是一直的。如果此變量是一個” 引用 “,那就可能和定義的不一致。
舉個例子:
func main() { var v [3]struct{} for i := range v { defer func() { fmt.Println(i) } () } }
執(zhí)行結(jié)果:
2
2
2
defer 后面跟的是一個閉包,i 是” 引用 “類型的變量, for 循環(huán)結(jié)束后 i 的值為 2,因此最后打印了 3 個 2。
再看一個例子:
type num int func (n num) print() {fmt.Println(n)} func (n *num) pprint() {fmt.Println(*n)} func main() { var n num defer n.print() defer n.pprint() defer func() {n.print()} () defer func() {n.pprint()} () n = 3 }
執(zhí)行結(jié)果為:
3
3
3
0
注意,defer 語句的執(zhí)行順序和定義順序相反。
第四個 defer 語句是閉包,引用外部函數(shù)的 n ,最終結(jié)果為 3;第三個 defer 語句同上;第二個 defer 語句,n 是引用,最終求值是 3;第一個 defer 語句,對 n 直接求值,開始的時候是 0,所以最后是 0。
再看一個延伸例子:
func main() { defer func() { fmt.Println("before return") }() if true { fmt.Println("during return") return } defer func() { fmt.Println("after return") }() }
運行結(jié)果如下:
during return
before return
解析:return 之后的 defer 函數(shù)不能被注冊,因此不能打印出 after return。
延伸示例則可以視為對 defer 的原理的利用。某些情況下,會故意用到 defer 的” 先求值,在延遲調(diào)用 “的性質(zhì)。想象這樣一個場景:在一個函數(shù)里,需要打開兩個文件進行合并操作,合并完成后,在函數(shù)結(jié)束前關(guān)閉打開的文件句柄:
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 函數(shù)定義的時候,參數(shù)就已經(jīng)復(fù)制進去了,之后,真正執(zhí)行 close() 函數(shù)的時候就剛好關(guān)閉的是正確的” 文件 “了,很巧妙,如果不這樣,將 f 當成函數(shù)參數(shù)傳遞進去的話,最后兩個語句關(guān)閉的就是同一個文件:都是最后打開的文件。
在調(diào)用 close() 函數(shù)的時候,要注意一點:先判斷調(diào)用主題是否為空,否則可能會解引用一個空指針,進而 panic。
3 如何拆解延遲語句
如果 defer 像前面介紹的那樣簡單,這個世界就完美了。但事情總是沒那么簡單,defer 用得不好,會陷入泥潭。
避免陷入泥潭的關(guān)鍵是必須深刻理解下面這條語句:
return xxx
上面這條語句經(jīng)過編譯之后,實際上生成了三條指令:
- 返回值 = xxx
- 調(diào)用 defer 函數(shù)
- 空的 return
第一步和第三步是 return 語句生成的指令,也就是說 return 并不是一條原子指令;第二步是 defer 定義的語句,這里可能會操作返回值,從而影響最終的結(jié)果。
看兩個例子,試著將 return 語句和 defer 語句拆解到正確的順序。
第一個例子:
func f()(r int) { t := 5 defer func() { t = t + 5 }() return t }
拆解后:
func f() (r int) { t := 5 // 1. 賦值指令 r = t // 2. defer 被插入到賦值與返回之間執(zhí)行,這個例子中返回值 r 沒被修改過 func() { t = t + 5 } // 3. 空的 return 指令 return }
這里第二步實際上并沒有操作返回值 r,因此,main 函數(shù)中調(diào)用 f() 得到 5。
第二個例子:
func f()(r int) { defer func(r int) { r = r + 5 }(r) return 1 }
拆解后:
func f()(r int) { // 1. 賦值 r = 1 // 2. 這里改的 r 是之前傳進去的r,不會改變要返回的那個 r 值 defer func(r int) { r = r + 5 }(r) // 3. 空的 return return 1 }
第二步,改變的是傳值進去的 r,是形參的一個復(fù)制值,不會影響實參 r。因此,main 函數(shù)中需要調(diào)用 f() 得到 1。
4 如何確定延遲語句的參數(shù)
defer 語句表達式的值在定義時就已經(jīng)確定了。下面可以通過三個不同的函數(shù)來理解:
func f1() { var err error defer fmt.Println(err) err = errors.New("defer1 error") return } func f2() { var err error defer func() { fmt.Println(err) }() err = errors.New("defer2 error") return } func f3() { var err error defer func(err error) { fmt.Println(err) }(err) err = errors.New("defer3 error") return } func main() { f1() f2() f3() }
運行結(jié)果:
<nil>
defer2 error
<nil>
第一和第三個函數(shù)中,因為作為參數(shù),err 在函數(shù)定義的時候就會求值,并且定義的時候 err 的值都是 nil,所以最后打印的結(jié)果都是 nil;第二個函數(shù)的參數(shù)其實也會在定義的時候求值,但是第二個例子中是一個閉包,它引用的變量 err 在執(zhí)行的時候值最終變成 defer2 error 了。
現(xiàn)實中第三個函數(shù)比較容易犯錯誤,在生產(chǎn)環(huán)境中,很容易寫出這樣的錯誤代碼,導(dǎo)致最后 defer 語句沒有起到作用,造成一些線上事故,要特別注意。
5 閉包是什么
閉包不是一句兩句話可以說清楚的,大家感興趣的話可以自行搜索這塊知識點,可以參考 閉包 和 閉包 這部分內(nèi)容自己研究。
閉包是由函數(shù)及其相關(guān)引用環(huán)境組合而成的實體,即:閉包 = 函數(shù) + 引用環(huán)境。
一般的函數(shù)都有函數(shù)名,而匿名函數(shù)沒有。匿名函數(shù)不能獨立存在,但可以直接調(diào)用或者賦值于某個變量。匿名函數(shù)也被稱為閉包,一個閉包繼承了函數(shù)聲明時的作用域。在 Go 語言中,所有的匿名函數(shù)都是閉包。
有個不太恰當?shù)睦樱嚎梢园验]包看成是一個類,一個閉包函數(shù)調(diào)用就是實例化一個類。閉包在運行時可以有多個實例,它會將同一個作用域里的變量和常量捕獲下來,無論閉包在什么地方被調(diào)用 (實例化) 時,都可以使用這些變量和常量。而且,閉包捕獲的變量和常量是引用傳遞,不是值傳遞。
舉個例子:
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 int return func(delta int) int { fmt.Printf("(%+v, %+v) - ", &x, x) x += delta return x } }
執(zhí)行結(jié)果是:
(0xc420014070, 0) - 1
(0xc420014070, 1) - 11
(0xc420014070, 11) - 111
------------------------
(0xc4200140b8, 0) - 1
(0xc4200140b8, 1) - 11
(0xc4200140b8, 11) – 111
閉包引用了 x 變量,a,b 可看作 2 個不同的實例,實例之間互不影響。實例內(nèi)部,x 變量是同一個地址,因此具有 “累加效應(yīng)”。
6 延遲語句如何配合恢復(fù)語句
Go 語言被詬病多次的就是它的 error,實際項目里經(jīng)常出現(xiàn)各種 error 滿天飛,正常的代碼邏輯里有很多 error 處理的代碼塊。函數(shù)總是會返回一個 error,留給調(diào)用者處理;而如果是致命的錯誤,比如程序執(zhí)行初始化的時候出問題,最好直接 panic 掉,避免上線運行后出更大的問題。
有些時候,需要從異常中恢復(fù)。比如服務(wù)器程序遇到嚴重問題,產(chǎn)生了 panic,這時至少可以在程序崩潰前做一些 “掃尾工作”,比如關(guān)閉客戶端的連接,防止客戶端一直等待等;并且單個請求導(dǎo)致的 panic,也不應(yīng)該影響整個服務(wù)器程序的運行。
panic 會停掉當前正在執(zhí)行的程序,而不只是當前線程。在這之前,它會有序地執(zhí)行完當前線程 defer 列表里的語句,其他協(xié)程里定義的 defer 語句不作保證。所以在 defer 里定義一個 recover 語句,防止程序直接掛掉,就可以起到類似 Java 里 try…catch 的效果。
注意,recover() 函數(shù)只在 defer 的函數(shù)中直接調(diào)用才有效。例如:
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") }
執(zhí)行結(jié)果:
defer here
defer caller
recover success. err: should set user env.
end of main function
defer main
代碼中的 panic 最終會被 recover 捕獲到。這樣的處理方式在一個 http server 的主流程常常會被用到。一次偶然的請求可能會觸發(fā)某個 bug,這時用 recover 捕獲 panic,穩(wěn)住主流程,不影響其他請求。
再看幾個延伸的示例。這些例子都與 recover() 函數(shù)的調(diào)用位置有關(guān)。
考慮一下寫法,程序是否能正確 recover 嗎?如果不能,原因是什么:
第一個例子:
func main() { defer f() panic(404) } func f() { if e := recover(); err != nil { fmt.Println("recover") return } }
能,在 defer 函數(shù)中調(diào)用,生效。
第二個例子:
func main() { recover() panic(404) }
不能。直接調(diào)用 recover,返回 nil。
第三個例子:
func main() { defer recover() panic(404) }
不能。要在 defer 函數(shù)中調(diào)用 recover。
第四個例子:
func main() { defer func() { if e := recover(); e != nil { fmt.Println(err) } }() panic(404) }
能,在 defer 函數(shù)中調(diào)用,生效。
第五個例子:
func main() { defer func() { defer func() { recover() }() }() panic() }
不能,多重 defer 嵌套。
7 defer 鏈如果被遍歷執(zhí)行
為了在退出函數(shù)前執(zhí)行一些資源清理的操作,例如關(guān)閉文件、釋放連接、釋放鎖資源等,會在函數(shù)里寫上多個 defer 語句,被 defered 的函數(shù),以 “先進后出” 的順序,在 RET 指令前得以執(zhí)行。
在一條函數(shù)調(diào)用鏈中,多個函數(shù)中會出現(xiàn)多個 defer 語句。例如:a()→b()→c() 中,每個函數(shù)里都有 defer 語句,而這些 defer 語句會創(chuàng)建對應(yīng)個數(shù)的 _defer 結(jié)構(gòu)體,這些結(jié)構(gòu)體以鏈表的形式 “掛” 在 G 結(jié)構(gòu)體下。
多個 _defer 結(jié)構(gòu)體形成一個鏈表,G 結(jié)構(gòu)體中某個字段指向此鏈表。
在編譯器的 “加持下”,defer 語句會先調(diào)用 deferporc 函數(shù),new 一個 _defer 結(jié)構(gòu)體,掛到 G 上。當然,調(diào)用 new 之前會優(yōu)先從當前 G 所綁定的 P 的 defer pool 里取,沒取到則會去全局的 defer pool 里取,實在沒有的話才新建一個。這是 Go runtime 里非常常見的操作,即設(shè)置多級緩存,提升運行效率。
在執(zhí)行 RET 指令之前 (注意不是 return 之前),調(diào)用 deferreturn 函數(shù)完成 _defer 鏈表的遍歷,執(zhí)行完這條鏈上所有被 defered 的函數(shù) (如關(guān)閉文件、釋放連接、釋放鎖資源等)。在 deferreturn 函數(shù)的最后,會使用 jmpdefer 跳轉(zhuǎn)到之前被 defered 的函數(shù),這時控制權(quán)從 runtime 轉(zhuǎn)移到了用戶自定義的函數(shù)。這只是執(zhí)行了一個被 defered 的函數(shù),那這條鏈上其他的被 defered 的 函數(shù),該如何得到執(zhí)行?
答案就是控制權(quán)會再次交給 runtime,并再次執(zhí)行 deferreturn 函數(shù),完成 defer 鏈表的遍歷。
8 為什么無法從父 goroutine 恢復(fù)子 goroutine 的 panic
對于這個問題,其實更普遍的問題是:為什么無法 recover 其他 goroutine 里產(chǎn)生的 panic?
可能會好奇為什么會有人希望從父 goroutine 中恢復(fù)子 goroutine 內(nèi)產(chǎn)生的 panic。這是因為,如果以下的情況發(fā)生在應(yīng)用程序內(nèi),那么整個進程必然退出:
func() { panic("die die die") }()
當然,上面的代碼是顯式的 panic,實際情況下,如果不注意編碼規(guī)范,極有可能觸發(fā)一些本可以避免的恐慌錯誤,例如訪問越界:
func() { a := make([]int, 1) println(a[1]) }()
發(fā)生這種恐慌錯誤對于服務(wù)端開發(fā)而言幾乎是致命的,因為開發(fā)者將無法預(yù)測服務(wù)的可用性,只能在錯誤發(fā)生時發(fā)現(xiàn)該錯誤,但這時服務(wù)不可用的損失已經(jīng)產(chǎn)生了。
那么,為什么不能從父 goroutine 中恢復(fù)子 goroutine 的 panic? 或者一般地說,為什么某個 goroutine 不能捕獲其他 goroutine 內(nèi)產(chǎn)生的 panic?
其實這個問題從 Go 誕生以來就一直被長久地討論,而答案可以簡單地認為是設(shè)計使然:因為 goroutine 被設(shè)計為一個獨立的代碼執(zhí)行單元,擁有自己的執(zhí)行棧,不與其他 goroutine 共享任何數(shù)據(jù)。這意味著,無法讓 goroutine 擁有返回值、也無法讓 goroutine 擁有自身的 ID 編號等。若需要與其他 goroutine 產(chǎn)生交互,要么可以使用 channel 的方式與其他 goroutine 進行通信,要么通過共享內(nèi)存同步方式對共享的內(nèi)存添加讀寫鎖。
那一點辦法也沒有了嗎?方法自然有,但并不是完美的方法,這里提供一種思路。例如,如果希望有一個全局的恐慌捕獲中心,那么可以通過創(chuàng)建一個恐慌通知 channel,并在產(chǎn)生恐慌時,通過 recover 字段將其恢復(fù),并將發(fā)生的錯誤通過 channel 通知給這個全局的恐慌通知器:
package main import ( "fmt" "time" ) var notifier chan interface{} func startGlobalPanicCapturing() { notifier = make(chan interface{}) go func() { for { select { case r := <- notifier: fmt.Println(r) } } }() } func main() { startGlobalPanicCapturing() // 產(chǎn)生恐慌,但該恐慌會被捕獲 Go(func() { a := make([]int, 1) println(a[1]) }) } // Go 是一個恐慌安全的 goroutine func Go(f func()) { go func() { defer func() { if r := recover(); r != nil { notifer <- i } }() }() }
上面的 func Go(f func()) 本質(zhì)上是對 go 關(guān)鍵字進行了一層封裝,確保在執(zhí)行并發(fā)單元前插入一個 defer,從而能夠保證恢復(fù)一些可恢復(fù)的錯誤。
之所以說這個方案并不完美,原因是如果函數(shù) f 內(nèi)部不再使用 Go 函數(shù)來創(chuàng)建 goroutine,而且含有繼續(xù)產(chǎn)生必然恐慌的代碼,那么仍然會出現(xiàn)不可恢復(fù)的情況。
func() {panic("die die die")}()
有人可能也許會想到,強制某個項目內(nèi)均使用 Go 函數(shù)不就好了?事情也并沒有這么簡單。因為除了可恢復(fù)的錯誤外,還有一些不可恢復(fù)的運行時恐慌 (例如并發(fā)讀寫 map),如果這類恐慌一旦發(fā)生,那么任何補救都是徒勞的。解決這類問題的根本途徑是提高程序員自身對語言的認識,多進行代碼測試,以及多通過運維技術(shù)來增強容災(zāi)機制。
到此這篇關(guān)于深入學(xué)習(xí)Go延遲語句的文章就介紹到這了,更多相關(guān)Go延遲語句內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言實現(xiàn)廣播式并發(fā)聊天服務(wù)器
本文主要介紹了Go語言實現(xiàn)廣播式并發(fā)聊天服務(wù)器,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-08-08詳解Golang實現(xiàn)http重定向https的方式
這篇文章主要介紹了詳解Golang實現(xiàn)http重定向https的方式,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08