Go defer 遇上 os.Exit 時失效
間單介紹 defer 使用與特性,並說明遇上 os.Exit 失效的原因
文章目錄
在撰寫 Go 程式過程中,我們可能需要在函式執行完畢後執行特定動作以確保不會發生異常,比方說讀取檔案完畢後需要關閉 file descriptor
。
為求程式簡潔,最常見的做法是呼叫 defer
進行處理。以下會對 defer
幾個特性做簡介,並且說明在什麼狀況下 deffered function
不會如預期般被執行。
若沒有耐心者,可直接跳到 當 defer 遇上 os.Exit 看本文重點 (笑)
寫程式時常會遇到需要讀寫檔案、建立連線等行為,在 Go 內類似的操作往往伴隨著關閉 file descriptor 或 connection 的相關操作。
file.Close()
除非偏好波動拳寫法以外
否則 early return 寫法往往會包含多個 return
,若此時需要在每個 return 前寫同樣的程式碼往往導致簡潔性被破壞。
func fileCopyv1(srcFile, dstFile string) (int64, error) {
src, err := os.Open(srcFile)
if err != nil {
return 0, err
}
dst, err := os.Create(dstFile)
if err != nil {
src.Close()
return 0, err
}
src.Close()
dst.Close()
return io.Copy(dst, src)
}
在這種狀況下 defer
就會派上很大的用途,在必要的地方寫好 cleanup function 後剩下的就可以交給 Go 來處理。
func fileCopyv2(srcFile, dstFile string) (int64, error) {
src, err := os.Open(srcFile)
if err != nil {
return 0, err
}
defer src.Close()
dst, err := os.Create(dstFile)
if err != nil {
return 0, err
}
defer dst.Close()
return io.Copy(dst, src)
}
另外,除了上述 cleanup 用途以外,若程式發生未知錯誤導致 panic
發生時,亦可以使用 defer + recover
的手法來確保服務能夠繼續運作。
func do() {
panic("PANIC")
}
func panicTest() {
defer func() {
// recover from unknown error
if err := recover(); err != nil {
fmt.Printf("Error occured: %v\n", err)
}
}()
do()
}
如此便利的關鍵字,在使用上當然也有幾個特性值得注意:
- deferred function 執行順序為 LIFO (Last-in, first-out)
- 在 surrounding function return 前執行
deferred function 的執行順序並非按照程式寫法由上往下執行(FIFO),而是類似 stack 逐一 pop 的 LIFO 順序執行。
Fig. 1 Go defer execution stack
這邊特別加粗 類似 stack
的原因在於,實際上 go 內部並未採用 stack 來儲存 deferred functions。
詳情請見補充說明 How Go stores deferred function calls。
從下面執行結果,可以看到除了 LIFO 特性以外,defer 2
執行是在整個 function return 後,而非離開 if 時就觸發。
func deferTest(input bool) {
defer log.Println("defer 1")
if input {
defer log.Println("defer 2")
}
log.Println("End of function")
return
}
$ go run main.go
2019/04/08 07:53:25 End of function
2019/04/08 07:53:25 defer 2
2019/04/08 07:53:25 defer 1
但在少數情況下 defer 會無法正常發揮效用,也是本文的重點。
近期研究如何在 Go 上實現 pluggable system,剛好發現 hashicorp 有推出其產品用的 go-plugin 套件。
有人提到每次執行範例後都會遺留 subprocess,程式面上並無發現特別異常的地方。
該清理遺留子程序的地方也都有利用 defer
做處理。
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: shared.Handshake,
Plugins: shared.PluginMap,
Cmd: exec.Command("sh", "-c", os.Getenv("KV_PLUGIN")),
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC, plugin.ProtocolGRPC},
})
defer client.Kill() // cleanup subprocess after program leaves
實際測試卻發現 subprocess 並沒有如預期般地被關閉,這當然就引起我的好奇心。
從 diff 中可以看到該問題是在加入 os.Exit(0)
後發生。
將其註解掉後,小鎮又恢復往常的和平。(感謝飛天小女警的努力)
從 godoc 針對 os.Exit 的用途描述:
Exit causes the current program to exit with the given status code. Conventionally, code zero indicates success, non-zero an error. The program terminates immediately; deferred functions are not run.
os.Exit
是立即性放棄執行,也就不符合上述的 surrounding function return 條件,因此不會觸發 deferred function。
等等,那 panic
不也是直接跳離程式嗎?
其實不管是隱式 (index out of bounds) 或顯式呼叫,panic 都會在離開前呼叫 deferred function 避免異常發生。
StackOverflow 的這則回覆寫得相當不錯,涵蓋兩者間的區別也提供對應的文件參考。
但下面另個回覆則提供相當好記的小技巧,來幫助開發者瞭解何時該用 panic
或 os.Exit
。
So basically panic is for you, a bad exit code is for your user.
意思是指 panic
是當問題發生在自己端時呼叫,一來可以除錯、二來可以利用 recover
確保服務運作,
而 os.Exit
則可以用來告知使用者說你那邊發生問題了,比如何執行指令後我們會利用其 exit code 檢測是否正常執行。
$ echo "test"
test
$ echo $?
0
(這種記法讓我想到這則推特寫的 4xx、5xx 代表含義 XDD)
HTTP status ranges in a nutshell:
— Steve Losh (@stevelosh) August 28, 2013
1xx: hold on
2xx: here you go
3xx: go away
4xx: you fucked up
5xx: I fucked up
- deferred function 執行順序為 LIFO
- 只在最外層 function return 前執行
- os.Exit 會立即離開不會 return,致使無法觸發 deferred function
實際上每次呼叫 defer 實際會改呼叫 runtime.deferproc,裡面會呼叫到 newdefer。
從 newdefer 可以看到,實際上是利用 slice 方式儲存,所以在程式執行時是按照呼叫順序 append 進去
systemstack(func() {
lock(&sched.deferlock)
for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
d := sched.deferpool[sc]
sched.deferpool[sc] = d.link
d.link = nil
pp.deferpool[sc] = append(pp.deferpool[sc], d) // append deferred function to defer pool
}
unlock(&sched.deferlock)
})
有趣的事情發生在離開時,freedefer 會將整個 slice 串成反向 linked list,因此 deferred function 才會以 LIFO 呼叫。
systemstack(func() {
var first, last *_defer
for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {
n := len(pp.deferpool[sc])
d := pp.deferpool[sc][n-1]
pp.deferpool[sc][n-1] = nil
pp.deferpool[sc] = pp.deferpool[sc][:n-1]
if first == nil {
first = d
} else {
last.link = d
}
last = d
}
lock(&sched.deferlock)
last.link = sched.deferpool[sc]
sched.deferpool[sc] = first
unlock(&sched.deferlock)
})
- https://golang.org/doc/effective_go.html#panic
- https://golang.org/pkg/os/#Exit
- https://logmatic.io/blog/5-common-golang-panics-that-ninjas-should-know/
- https://stackoverflow.com/questions/28472922/when-to-use-os-exit-and-panic/
- https://ieevee.com/tech/2017/11/23/go-panic.html
相關文章
文章內容的轉載、重製、發佈,請註明出處: https://tachingchen.com/tw/
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Pinterest
Email