Golang 中 defer Close() 的潛在風險

作為一名 Gopher,我們很容易形成一個編程慣例:每當有一個實現(xiàn)了 io.Closer 接口的對象 x 時,在得到對象并檢查錯誤之后,會立即使用 defer x.Close() 以保證函數(shù)返回時 x 對象的關閉 镶殷。以下給出兩個慣用寫法例子。

  • HTTP 請求
resp, err := http.Get("https://golang.google.cn/")
if err != nil {
    return err
}
defer resp.Body.Close()
// The following code: handle resp
  • 訪問文件
f, err := os.Open("/home/golangshare/gopher.txt")
if err != nil {
    return err
}
defer f.Close()
// The following code: handle f

存在問題

實際上微酬,這種寫法是存在潛在問題的绘趋。defer x.Close() 會忽略它的返回值,但在執(zhí)行 x.Close() 時颗管,我們并不能保證 x 一定能正常關閉陷遮,萬一它返回錯誤應該怎么辦?這種寫法垦江,會讓程序有可能出現(xiàn)非常難以排查的錯誤帽馋。

那么,Close() 方法會返回什么錯誤呢比吭?在 POSIX 操作系統(tǒng)中绽族,例如 Linux 或者 maxOS,關閉文件的 Close() 函數(shù)最終是調用了系統(tǒng)方法 close()衩藤,我們可以通過 man close 手冊吧慢,查看 close() 可能會返回什么錯誤

ERRORS
     The close() system call will fail if:

     [EBADF]            fildes is not a valid, active file descriptor.

     [EINTR]            Its execution was interrupted by a signal.

     [EIO]              A previously-uncommitted write(2) encountered an
                        input/output error.

錯誤 EBADF 表示無效文件描述符 fd,與本文中的情況無關赏表;EINTR 是指的 Unix 信號打斷检诗;那么本文中可能存在的錯誤是 EIO匈仗。

EIO 的錯誤是指未提交讀,這是什么錯誤呢逢慌?

計算機存儲體系.png

EIO 錯誤是指文件的 write() 的讀還未提交時就調用了 close() 方法悠轩。

上圖是一個經典的計算機存儲器層級結構,在這個層次結構中攻泼,從上至下火架,設備的訪問速度越來越慢,容量越來越大忙菠。存儲器層級結構的主要思想是上一層的存儲器作為低一層存儲器的高速緩存距潘。

CPU 訪問寄存器會非常之快,相比之下只搁,訪問 RAM 就會很慢,而訪問磁盤或者網絡俭尖,那意味著就是蹉跎光陰氢惋。如果每個 write() 調用都將數(shù)據(jù)同步地提交到磁盤,那么系統(tǒng)的整體性能將會極度降低稽犁,而我們的計算機是不會這樣工作的焰望。當我們調用 write() 時,數(shù)據(jù)并沒有立即被寫到目標載體上已亥,計算機存儲器每層載體都在緩存數(shù)據(jù)熊赖,在合適的時機下,將數(shù)據(jù)刷到下一層載體虑椎,這將寫入調用的同步震鹉、緩慢、阻塞的同步轉為了快速捆姜、異步的過程传趾。

這樣看來,EIO 錯誤的確是我們需要提防的錯誤泥技。這意味著如果我們嘗試將數(shù)據(jù)保存到磁盤浆兰,在 defer x.Close() 執(zhí)行時,操作系統(tǒng)還并未將數(shù)據(jù)刷到磁盤珊豹,這時我們應該獲取到該錯誤提示(只要數(shù)據(jù)還未落盤簸呈,那數(shù)據(jù)就沒有持久化成功,它就是有可能丟失的店茶,例如出現(xiàn)停電事故蜕便,這部分數(shù)據(jù)就永久消失了,且我們會毫不知情)忽妒。但是按照上文的慣例寫法玩裙,我們程序得到的是 nil 錯誤兼贸。

解決方案

我們針對關閉文件的情況,來探討幾種可行性改造方案

  • 第一種方案吃溅,那就是不使用 defer
func solution01() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }

    if _, err = io.WriteString(f, "hello gopher"); err != nil {
        f.Close()
        return err
    }

    return f.Close()
}

這種寫法就需要我們在 io.WriteString 執(zhí)行失敗時溶诞,明確調用 f.Close() 進行關閉。但是這種方案决侈,需要在每個發(fā)生錯誤的地方都要加上關閉語句 f.Close()螺垢,如果對 f 的寫操作 case 較多,容易存在遺漏關閉文件的風險赖歌。

  • 第二種方案是枉圃,通過命名返回值 err 和閉包來處理
func solution02() (err error) {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return
    }

    defer func() {
        closeErr := f.Close()
        if err == nil {
            err = closeErr
        }
    }()

    _, err = io.WriteString(f, "hello gopher")
    return
}

這種方案解決了方案一中忘記關閉文件的風險,如果有更多 if err !=nil 的條件分支庐冯,這種模式可以有效降低代碼行數(shù)孽亲。

  • 第三種方案是,在函數(shù)最后 return 語句之前展父,顯示調用一次 f.Close()
func solution03() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }
    defer f.Close()

    if _, err := io.WriteString(f, "hello gopher"); err != nil {
        return err
    }

    if err := f.Close(); err != nil {
        return err
    }
    return nil
}

這種解決方案能在 io.WriteString 發(fā)生錯誤時返劲,由于 defer f.Close() 的存在能得到 close 調用。也能在 io.WriteString 未發(fā)生錯誤栖茉,但緩存未刷新到磁盤時预皇,得到 err := f.Close() 的錯誤乳讥,而且由于 defer f.Close() 并不會返回錯誤,所以并不擔心兩次 Close() 調用會將錯誤覆蓋。

  • 最后一種方案是治唤,函數(shù) return 時執(zhí)行 f.Sync()
func solution04() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }
    defer f.Close()

    if _, err = io.WriteString(f, "hello world"); err != nil {
        return err
    }

    return f.Sync()
}

由于調用 close() 是最后一次獲取操作系統(tǒng)返回錯誤的機會泽裳,但是在我們關閉文件時笋额,緩存不一定被會刷到磁盤上冯袍。那么,我們可以調用 f.Sync() (其內部調用系統(tǒng)函數(shù) fsync )強制性讓內核將緩存持久到磁盤上去梨睁。

// Sync commits the current contents of the file to stable storage.
// Typically, this means flushing the file system's in-memory copy
// of recently written data to disk.
func (f *File) Sync() error {
    if err := f.checkValid("sync"); err != nil {
        return err
    }
    if e := f.pfd.Fsync(); e != nil {
        return f.wrapErr("sync", e)
    }
    return nil
}

由于 fsync 的調用鲸睛,這種模式能很好地避免 close 出現(xiàn)的 EIO∑潞兀可以預見的是官辈,由于強制性刷盤,這種方案雖然能很好地保證數(shù)據(jù)安全性遍坟,但是在執(zhí)行效率上卻會大打折扣拳亿。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市愿伴,隨后出現(xiàn)的幾起案子肺魁,更是在濱河造成了極大的恐慌,老刑警劉巖隔节,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹅经,死亡現(xiàn)場離奇詭異寂呛,居然都是意外死亡,警方通過查閱死者的電腦和手機瘾晃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門贷痪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蹦误,你說我怎么就攤上這事劫拢。” “怎么了强胰?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵舱沧,是天一觀的道長。 經常有香客問我偶洋,道長熟吏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任玄窝,我火速辦了婚禮分俯,結果婚禮上,老公的妹妹穿的比我還像新娘哆料。我一直安慰自己,他們只是感情好吗铐,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布东亦。 她就那樣靜靜地躺著,像睡著了一般唬渗。 火紅的嫁衣襯著肌膚如雪典阵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天镊逝,我揣著相機與錄音壮啊,去河邊找鬼。 笑死撑蒜,一個胖子當著我的面吹牛歹啼,可吹牛的內容都是我干的。 我是一名探鬼主播座菠,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼狸眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了浴滴?” 一聲冷哼從身側響起拓萌,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎升略,沒想到半個月后微王,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屡限,經...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年炕倘,在試婚紗的時候發(fā)現(xiàn)自己被綠了钧大。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡激才,死狀恐怖拓型,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情瘸恼,我是刑警寧澤劣挫,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站东帅,受9級特大地震影響压固,放射性物質發(fā)生泄漏。R本人自食惡果不足惜靠闭,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一帐我、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧愧膀,春花似錦拦键、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蟀悦,卻和暖如春媚朦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背日戈。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工询张, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人浙炼。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓份氧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親弯屈。 傳聞我的和親對象是個殘疾皇子半火,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內容

  • 當關閉一個可寫文件時,該操作會又系統(tǒng)調用call完成季俩,該調用可能會返回一下錯誤代碼钮糖。 當EIO 錯誤發(fā)生時,寫入文...
    kker閱讀 700評論 0 0
  • 函數(shù) Golang函數(shù)特點 無需聲明原型支持多返回值不定參數(shù)傳參 也就是函數(shù)的參數(shù)個數(shù)不是固定的 但是后面的類型是...
    TZX_0710閱讀 621評論 0 0
  • Golang作為一門新的編程語言,它借鑒了現(xiàn)有語言的思想但擁有著不同尋常的特性店归,使得有效的Go程序在性質上不同于其...
    云時代的運維開發(fā)閱讀 894評論 0 0
  • 參考:http://c.biancheng.net/view/61.html 關鍵點 希望通過下面的關鍵詞阎抒,能夠實...
    碼二哥閱讀 225評論 0 0
  • 一、錯誤異常 《快學 Go 語言》第 10 課 —— 錯誤與異常Go 語言的異常處理語法絕對是獨樹一幟消痛,在我見過的...
    合肥黑閱讀 1,094評論 0 3