你有考慮過Defer Close() 的風險嗎
本文轉(zhuǎn)載自微信公眾號「Golang技術(shù)分享」,作者機器鈴砍菜刀 。轉(zhuǎn)載本文請聯(lián)系Golang技術(shù)分享公眾號。
作為一名 Gopher,我們很容易形成一個編程慣例:每當有一個實現(xiàn)了 io.Closer 接口的對象 x 時,在得到對象并檢查錯誤之后,會立即使用 defer x.Close() 以保證函數(shù)返回時 x 對象的關(guān)閉 。以下給出兩個慣用寫法例子。
HTTP 請求
- 1resp, err := http.Get("https://golang.google.cn/")
- 2if err != nil {
- 3 return err
- 4}
- 5defer resp.Body.Close()
- 6// The following code: handle resp
訪問文件
- 1f, err := os.Open("/home/golangshare/gopher.txt")
- 2if err != nil {
- 3 return err
- 4}
- 5defer f.Close()
- 6// The following code: handle f
存在問題
實際上,這種寫法是存在潛在問題的。defer x.Close() 會忽略它的返回值,但在執(zhí)行 x.Close() 時,我們并不能保證 x 一定能正常關(guān)閉,萬一它返回錯誤應該怎么辦?這種寫法,會讓程序有可能出現(xiàn)非常難以排查的錯誤。
那么,Close() 方法會返回什么錯誤呢?在 POSIX 操作系統(tǒng)中,例如 Linux 或者 maxOS,關(guān)閉文件的 Close() 函數(shù)最終是調(diào)用了系統(tǒng)方法 close(),我們可以通過 man close 手冊,查看 close() 可能會返回什么錯誤
- 1ERRORS
- 2 The close() system call will fail if:
- 3
- 4 [EBADF] fildes is not a valid, active file descriptor.
- 5
- 6 [EINTR] Its execution was interrupted by a signal.
- 7
- 8 [EIO] A previously-uncommitted write(2) encountered an
- 9 input/output error.
錯誤 EBADF 表示無效文件描述符 fd,與本文中的情況無關(guān);EINTR 是指的 Unix 信號打斷;那么本文中可能存在的錯誤是 EIO。
EIO 的錯誤是指未提交讀,這是什么錯誤呢?
計算機存儲層次結(jié)構(gòu)
EIO 錯誤是指文件的 write() 的讀還未提交時就調(diào)用了 close() 方法。
上圖是一個經(jīng)典的計算機存儲器層級結(jié)構(gòu),在這個層次結(jié)構(gòu)中,從上至下,設(shè)備的訪問速度越來越慢,容量越來越大。存儲器層級結(jié)構(gòu)的主要思想是上一層的存儲器作為低一層存儲器的高速緩存。
CPU 訪問寄存器會非常之快,相比之下,訪問 RAM 就會很慢,而訪問磁盤或者網(wǎng)絡,那意味著就是蹉跎光陰。如果每個 write() 調(diào)用都將數(shù)據(jù)同步地提交到磁盤,那么系統(tǒng)的整體性能將會極度降低,而我們的計算機是不會這樣工作的。當我們調(diào)用 write() 時,數(shù)據(jù)并沒有立即被寫到目標載體上,計算機存儲器每層載體都在緩存數(shù)據(jù),在合適的時機下,將數(shù)據(jù)刷到下一層載體,這將寫入調(diào)用的同步、緩慢、阻塞的同步轉(zhuǎn)為了快速、異步的過程。
這樣看來,EIO 錯誤的確是我們需要提防的錯誤。這意味著如果我們嘗試將數(shù)據(jù)保存到磁盤,在 defer x.Close() 執(zhí)行時,操作系統(tǒng)還并未將數(shù)據(jù)刷到磁盤,這時我們應該獲取到該錯誤提示(只要數(shù)據(jù)還未落盤,那數(shù)據(jù)就沒有持久化成功,它就是有可能丟失的,例如出現(xiàn)停電事故,這部分數(shù)據(jù)就永久消失了,且我們會毫不知情)。但是按照上文的慣例寫法,我們程序得到的是 nil 錯誤。
解決方案
我們針對關(guān)閉文件的情況,來探討幾種可行性改造方案
- 第一種方案,那就是不使用 defer
- 1func solution01() error {
- 2 f, err := os.Create("/home/golangshare/gopher.txt")
- 3 if err != nil {
- 4 return err
- 5 }
- 6
- 7 if _, err = io.WriteString(f, "hello gopher"); err != nil {
- 8 f.Close()
- 9 return err
- 10 }
- 11
- 12 return f.Close()
- 13}
這種寫法就需要我們在 io.WriteString 執(zhí)行失敗時,明確調(diào)用 f.Close() 進行關(guān)閉。但是這種方案,需要在每個發(fā)生錯誤的地方都要加上關(guān)閉語句 f.Close(),如果對 f 的寫操作 case 較多,容易存在遺漏關(guān)閉文件的風險。
- 第二種方案是,通過命名返回值 err 和閉包來處理
- 1func solution02() (err error) {
- 2 f, err := os.Create("/home/golangshare/gopher.txt")
- 3 if err != nil {
- 4 return
- 5 }
- 6
- 7 defer func() {
- 8 closeErr := f.Close()
- 9 if err == nil {
- 10 err = closeErr
- 11 }
- 12 }()
- 13
- 14 _, err = io.WriteString(f, "hello gopher")
- 15 return
- 16}
這種方案解決了方案一中忘記關(guān)閉文件的風險,如果有更多 if err !=nil 的條件分支,這種模式可以有效降低代碼行數(shù)。
- 第三種方案是,在函數(shù)最后 return 語句之前,顯示調(diào)用一次 f.Close()
- 1func solution03() error {
- 2 f, err := os.Create("/home/golangshare/gopher.txt")
- 3 if err != nil {
- 4 return err
- 5 }
- 6 defer f.Close()
- 7
- 8 if _, err := io.WriteString(f, "hello gopher"); err != nil {
- 9 return err
- 10 }
- 11
- 12 if err := f.Close(); err != nil {
- 13 return err
- 14 }
- 15 return nil
- 16}
這種解決方案能在 io.WriteString 發(fā)生錯誤時,由于 defer f.Close() 的存在能得到 close 調(diào)用。也能在 io.WriteString 未發(fā)生錯誤,但緩存未刷新到磁盤時,得到 err := f.Close() 的錯誤,而且由于 defer f.Close() 并不會返回錯誤,所以并不擔心兩次 Close() 調(diào)用會將錯誤覆蓋。
- 最后一種方案是,函數(shù) return 時執(zhí)行 f.Sync()
- 1func solution04() error {
- 2 f, err := os.Create("/home/golangshare/gopher.txt")
- 3 if err != nil {
- 4 return err
- 5 }
- 6 defer f.Close()
- 7
- 8 if _, err = io.WriteString(f, "hello world"); err != nil {
- 9 return err
- 10 }
- 11
- 12 return f.Sync()
- 13}
由于調(diào)用 close() 是最后一次獲取操作系統(tǒng)返回錯誤的機會,但是在我們關(guān)閉文件時,緩存不一定被會刷到磁盤上。那么,我們可以調(diào)用 f.Sync() (其內(nèi)部調(diào)用系統(tǒng)函數(shù) fsync )強制性讓內(nèi)核將緩存持久到磁盤上去。
- 1// Sync commits the current contents of the file to stable storage.
- 2// Typically, this means flushing the file system's in-memory copy
- 3// of recently written data to disk.
- 4func (f *File) Sync() error {
- 5 if err := f.checkValid("sync"); err != nil {
- 6 return err
- 7 }
- 8 if e := f.pfd.Fsync(); e != nil {
- 9 return f.wrapErr("sync", e)
- 10 }
- 11 return nil
- 12}
由于 fsync 的調(diào)用,這種模式能很好地避免 close 出現(xiàn)的 EIO??梢灶A見的是,由于強制性刷盤,這種方案雖然能很好地保證數(shù)據(jù)安全性,但是在執(zhí)行效率上卻會大打折扣。




























