Go 專欄|錯(cuò)誤處理:defer,panic 和 recover

原文鏈接: Go 專欄|錯(cuò)誤處理:defer,panic 和 recover

最近校招又開始了,我也接到了一些面試工作,當(dāng)我問「你覺得自己有什么優(yōu)勢(shì)」時(shí),十個(gè)人里有八個(gè)的回答里會(huì)有一條「精力充沛,能加班」。

怪不得國家都給認(rèn)證了:新生代農(nóng)民工。合著我們這根本就不是什么腦力勞動(dòng)者,而是靠出賣體力的苦勞力。

好了,廢話不多說,肝文還確實(shí)需要體力。

這篇來說說 Go 的錯(cuò)誤處理。

錯(cuò)誤處理

錯(cuò)誤處理相當(dāng)重要,合理地拋出并記錄錯(cuò)誤能在排查問題時(shí)起到事半功倍的作用。

Go 中有關(guān)于錯(cuò)誤處理的標(biāo)準(zhǔn)模式,即 error 接口,定義如下:

type error interface {
    Error() string
}

大部分函數(shù),如果需要返回錯(cuò)誤的話,基本都會(huì)將 error 作為多個(gè)返回值的最后一個(gè),舉個(gè)例子:

package main

import "fmt"

func main() {
    n, err := echo(10)
    if err != nil {
        fmt.Println("error: " + err.Error())
    } else {
        fmt.Println(n)
    }
}

func echo(param int) (int, error) {
    return param, nil
}

我們也可以使用自定義的 error 類型,比如調(diào)用標(biāo)準(zhǔn)庫的 os.Stat 方法,返回的錯(cuò)誤就是自定義類型:

type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

暫時(shí)看不懂也沒有關(guān)系,等學(xué)會(huì)了接口之后,再回過頭來看這段代碼,應(yīng)該就豁然開朗了。

defer

延遲函數(shù)調(diào)用,defer 后邊會(huì)接一個(gè)函數(shù),但該函數(shù)不會(huì)立刻被執(zhí)行,而是等到包含它的程序返回時(shí)(包含它的函數(shù)執(zhí)行了 return 語句、運(yùn)行到函數(shù)結(jié)尾自動(dòng)返回、對(duì)應(yīng)的 goroutine panic),defer 函數(shù)才會(huì)被執(zhí)行。

通常用于資源釋放、打印日志、異常捕獲等。

func main() {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    /**
     * 這里defer要寫在err判斷的后邊而不是os.Open后邊
     * 如果資源沒有獲取成功,就沒有必要對(duì)資源執(zhí)行釋放操作
     * 如果err不為nil而執(zhí)行資源執(zhí)行釋放操作,有可能導(dǎo)致panic
     */
    defer f.Close()
}

defer 語句經(jīng)常成對(duì)出現(xiàn),比如打開和關(guān)閉,連接和斷開,加鎖和解鎖。

defer 語句在 return 語句之后執(zhí)行。

package main

import (
    "fmt"
)

func main() {
    fmt.Println(triple(4)) // 12
}

func double(x int) (result int) {
    defer func() {
        fmt.Printf("double(%d) = %d\n", x, result)
    }()

    return x + x
}

func triple(x int) (result int) {
    defer func() {
        result += x
    }()

    return double(x)
}

切勿在 for 循環(huán)中使用 defer 語句,因?yàn)?defer 語句不到函數(shù)的最后一刻是不會(huì)執(zhí)行的,所以下面這段代碼很可能會(huì)用盡所有文件描述符。

for _, filename := range filenames {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()
}

一種解決辦法是將循環(huán)體單獨(dú)寫一個(gè)函數(shù),這樣每次循環(huán)的時(shí)候都會(huì)調(diào)用關(guān)閉函數(shù)。

for _, filename := range filenames {
    if err := doFile(filename); err != nil {
        return err
    }
}

func doFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()
}

defer 語句的執(zhí)行是按調(diào)用 defer 語句的倒序執(zhí)行。

package main

import (
    "fmt"
)

func main() {
    defer func() {
        fmt.Println("first")
    }()

    defer func() {
        fmt.Println("second")
    }()

    fmt.Println("done")
}

輸出:

done
second
first

panic 和 recover

一般情況下,在程序里記錄錯(cuò)誤日志,就可以幫助我們?cè)谂龅疆惓r(shí)快速定位問題。

但還有一些錯(cuò)誤比較嚴(yán)重的,比如數(shù)組越界訪問,程序會(huì)主動(dòng)調(diào)用 panic 來拋出異常,然后程序退出。

如果不想程序退出的話,可以使用 recover 函數(shù)來捕獲并恢復(fù)。

感覺挺不好理解的,但仔細(xì)想想其實(shí)和 try-catch 也沒什么區(qū)別。

先來看看兩個(gè)函數(shù)的定義:

func panic(interface{})
func recover() interface{}

panic 參數(shù)類型是 interface{},所以可以接收任意參數(shù)類型,比如:

panic(404)
panic("network broken")
panic(Error("file not exists"))

recover 需要在 defer 函數(shù)中執(zhí)行,舉個(gè)例子:

package main

import (
    "fmt"
)

func main() {
    G()
}

func G() {
    defer func() {
        fmt.Println("c")
    }()
    F()
    fmt.Println("繼續(xù)執(zhí)行")
}

func F() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("捕獲異常:", err)
        }
        fmt.Println("b")
    }()
    panic("a")
}

輸出:

捕獲異常: a
b
繼續(xù)執(zhí)行
c

F() 中拋出異常被捕獲,G() 還可以正常繼續(xù)執(zhí)行。如果 F() 沒有捕獲的話,那么 panic 會(huì)向上傳遞,直接導(dǎo)致 G() 異常,然后程序直接退出。

還有一個(gè)場(chǎng)景就是我們自己在調(diào)試程序時(shí),可以使用 panic 來中斷程序,拋出異常,用于排查問題。

這個(gè)就不舉例了,反正是我們自己調(diào)試,怎么爽怎么來就行了。

總結(jié)

錯(cuò)誤處理在開發(fā)過程中至關(guān)重要,好的錯(cuò)誤處理可以使程序更加健壯。而且將錯(cuò)誤信息清晰地記錄日志,在排查問題時(shí)非常有用。

Go 中使用 error 類型進(jìn)行錯(cuò)誤處理,還可以在此基礎(chǔ)上自定義錯(cuò)誤類型。

使用 defer 語句進(jìn)行延遲調(diào)用,用來關(guān)閉或釋放資源。

使用 panicrecover 來拋出錯(cuò)誤和恢復(fù)。

使用 panic 一般有兩種情況:

  1. 程序遇到無法執(zhí)行的錯(cuò)誤時(shí),主動(dòng)調(diào)用 panic 結(jié)束運(yùn)行;
  2. 在調(diào)試程序時(shí),主動(dòng)調(diào)用 panic 結(jié)束運(yùn)行,根據(jù)拋出的錯(cuò)誤信息來定位問題。

為了程序的健壯性,可以使用 recover 捕獲錯(cuò)誤,恢復(fù)程序運(yùn)行。


文章中的腦圖和源碼都上傳到了 GitHub,有需要的同學(xué)可自行下載。

地址: https://github.com/yongxinz/gopher/tree/main/sc

關(guān)注公眾號(hào) AlwaysBeta,回復(fù)「goebook」領(lǐng)取 Go 編程經(jīng)典書籍。

Go 專欄文章列表:

  1. Go 專欄|開發(fā)環(huán)境搭建以及開發(fā)工具 VS Code 配置
  2. Go 專欄|變量和常量的聲明與賦值
  3. Go 專欄|基礎(chǔ)數(shù)據(jù)類型:整數(shù)、浮點(diǎn)數(shù)、復(fù)數(shù)、布爾值和字符串
  4. Go 專欄|復(fù)合數(shù)據(jù)類型:數(shù)組和切片 slice
  5. Go 專欄|復(fù)合數(shù)據(jù)類型:字典 map 和 結(jié)構(gòu)體 struct
  6. Go 專欄|流程控制,一網(wǎng)打盡
  7. Go 專欄|函數(shù)那些事
  8. Go 專欄|錯(cuò)誤處理:defer,panic 和 recover
  9. Go 專欄|說說方法
  10. Go 專欄|接口 interface
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容