go學(xué)習(xí)十六·錯(cuò)誤處理

錯(cuò)誤處理

在實(shí)際工程項(xiàng)目中,我們希望通過程序的錯(cuò)誤信息快速定位問題,但是又不喜歡錯(cuò)誤處理代碼寫的冗余而又啰嗦。Go語言沒有提供像Java、C#語言中的try...catch異常處理方式,而是通過函數(shù)返回值逐層往上拋。這種設(shè)計(jì),鼓勵(lì)工程師在代碼中顯式的檢查錯(cuò)誤,而非忽略錯(cuò)誤,好處就是避免漏掉本應(yīng)處理的錯(cuò)誤。但是帶來一個(gè)弊端,讓代碼啰嗦。

1.1 什么是錯(cuò)誤

錯(cuò)誤是什么?

錯(cuò)誤指的是可能出現(xiàn)問題的地方出現(xiàn)了問題。比如打開一個(gè)文件時(shí)失敗,這種情況在人們的意料之中 。

而異常指的是不應(yīng)該出現(xiàn)問題的地方出現(xiàn)了問題。比如引用了空指針,這種情況在人們的意料之外??梢姡e(cuò)誤是業(yè)務(wù)過程的一部分,而異常不是 。

Go中的錯(cuò)誤也是一種類型。錯(cuò)誤用內(nèi)置的error 類型表示。就像其他類型的,如int,float64,。錯(cuò)誤值可以存儲(chǔ)在變量中,從函數(shù)中返回,等等。

1.2 演示錯(cuò)誤

讓我們從一個(gè)示例程序開始,這個(gè)程序嘗試打開一個(gè)不存在的文件。

示例代碼:

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err := os.Open("/test.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
  //根據(jù)f進(jìn)行文件的讀或?qū)?    fmt.Println(f.Name(), "opened successfully")
}

在os包中有打開文件的功能函數(shù):

func Open(name string) (file *File, err error)

如果文件已經(jīng)成功打開,那么Open函數(shù)將返回文件處理。如果在打開文件時(shí)出現(xiàn)錯(cuò)誤,將返回一個(gè)非nil錯(cuò)誤。

如果一個(gè)函數(shù)或方法返回一個(gè)錯(cuò)誤,那么按照慣例,它必須是函數(shù)返回的最后一個(gè)值。因此,Open 函數(shù)返回的值是最后一個(gè)值。

處理錯(cuò)誤的慣用方法是將返回的錯(cuò)誤與nil進(jìn)行比較。nil值表示沒有發(fā)生錯(cuò)誤,而非nil值表示出現(xiàn)錯(cuò)誤。在我們的例子中,我們檢查錯(cuò)誤是否為nil。如果它不是nil,我們只需打印錯(cuò)誤并從主函數(shù)返回。

運(yùn)行結(jié)果:

open /test.txt: No such file or directory

我們得到一個(gè)錯(cuò)誤,說明該文件不存在。

1.3 錯(cuò)誤類型表示

Go 語言通過內(nèi)置的錯(cuò)誤接口提供了非常簡單的錯(cuò)誤處理機(jī)制。

讓我們?cè)偕钊胍稽c(diǎn),看看如何定義錯(cuò)誤類型的構(gòu)建。錯(cuò)誤是一個(gè)帶有以下定義的接口類型,

type error interface {
    Error() string
}

它包含一個(gè)帶有Error()字符串的方法。任何實(shí)現(xiàn)這個(gè)接口的類型都可以作為一個(gè)錯(cuò)誤使用。這個(gè)方法提供了對(duì)錯(cuò)誤的描述。

當(dāng)打印錯(cuò)誤時(shí),fmt.Println函數(shù)在內(nèi)部調(diào)用Error() 方法來獲取錯(cuò)誤的描述。這就是錯(cuò)誤描述是如何在一行中打印出來的。

從錯(cuò)誤中提取更多信息的不同方法

既然我們知道錯(cuò)誤是一種接口類型,那么讓我們看看如何提取更多關(guān)于錯(cuò)誤的信息。

在上面的例子中,我們僅僅是打印了錯(cuò)誤的描述。如果我們想要的是導(dǎo)致錯(cuò)誤的文件的實(shí)際路徑。一種可能的方法是解析錯(cuò)誤字符串。這是我們程序的輸出,

open /test.txt: No such file or directory  

我們可以解析這個(gè)錯(cuò)誤消息并從中獲取文件路徑"/test.txt"。但這是一個(gè)糟糕的方法。在新版本的語言中,錯(cuò)誤描述可以隨時(shí)更改,我們的代碼將會(huì)中斷。

是否有辦法可靠地獲取文件名?答案是肯定的,它可以做到,標(biāo)準(zhǔn)Go庫使用不同的方式提供更多關(guān)于錯(cuò)誤的信息。讓我們一看一看。

1.斷言底層結(jié)構(gòu)類型并從結(jié)構(gòu)字段獲取更多信息

如果仔細(xì)閱讀打開函數(shù)的文檔,可以看到它返回的是PathError類型的錯(cuò)誤。PathError是一個(gè)struct類型,它在標(biāo)準(zhǔn)庫中的實(shí)現(xiàn)如下,

type PathError struct {  
    Op   string
    Path string
    Err  error
}

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

從上面的代碼中,您可以理解PathError通過聲明Error()string方法實(shí)現(xiàn)了錯(cuò)誤接口。該方法連接操作、路徑和實(shí)際錯(cuò)誤并返回它。這樣我們就得到了錯(cuò)誤信息,

open /test.txt: No such file or directory 

PathError結(jié)構(gòu)的路徑字段包含導(dǎo)致錯(cuò)誤的文件的路徑。讓我們修改上面寫的程序,并打印出路徑。

修改代碼:

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err := os.Open("/test.txt")
    if err, ok := err.(*os.PathError); ok {
        fmt.Println("File at path", err.Path, "failed to open")
        return
    }
    fmt.Println(f.Name(), "opened successfully")
}

在上面的程序中,我們使用類型斷言獲得錯(cuò)誤接口的基本值。然后我們用錯(cuò)誤來打印路徑.這個(gè)程序輸出,

File at path /test.txt failed to open  

  1. 斷言底層結(jié)構(gòu)類型,并使用方法獲取更多信息

獲得更多信息的第二種方法是斷言底層類型,并通過調(diào)用struct類型的方法獲取更多信息。

示例代碼:

type DNSError struct {  
    ...
}

func (e *DNSError) Error() string {  
    ...
}
func (e *DNSError) Timeout() bool {  
    ... 
}
func (e *DNSError) Temporary() bool {  
    ... 
}

從上面的代碼中可以看到,DNSError struct有兩個(gè)方法Timeout() bool和Temporary() bool,它們返回一個(gè)布爾值,表示錯(cuò)誤是由于超時(shí)還是臨時(shí)的。

讓我們編寫一個(gè)斷言*DNSError類型的程序,并調(diào)用這些方法來確定錯(cuò)誤是臨時(shí)的還是超時(shí)的。

package main

import (  
    "fmt"
    "net"
)

func main() {  
    addr, err := net.LookupHost("golangbot123.com")
    if err, ok := err.(*net.DNSError); ok {
        if err.Timeout() {
            fmt.Println("operation timed out")
        } else if err.Temporary() {
            fmt.Println("temporary error")
        } else {
            fmt.Println("generic error: ", err)
        }
        return
    }
    fmt.Println(addr)
}

在上面的程序中,我們正在嘗試獲取一個(gè)無效域名的ip地址,這是一個(gè)無效的域名。golangbot123.com。我們通過聲明它來輸入*net.DNSError來獲得錯(cuò)誤的潛在價(jià)值。

在我們的例子中,錯(cuò)誤既不是暫時(shí)的,也不是由于超時(shí),因此程序會(huì)打印出來,

generic error:  lookup golangbot123.com: no such host  

如果錯(cuò)誤是臨時(shí)的或超時(shí)的,那么相應(yīng)的If語句就會(huì)執(zhí)行,我們可以適當(dāng)?shù)靥幚硭?/p>

3.直接比較

獲得更多關(guān)于錯(cuò)誤的詳細(xì)信息的第三種方法是直接與類型錯(cuò)誤的變量進(jìn)行比較。讓我們通過一個(gè)例子來理解這個(gè)問題。

filepath包的Glob函數(shù)用于返回與模式匹配的所有文件的名稱。當(dāng)模式出現(xiàn)錯(cuò)誤時(shí),該函數(shù)將返回一個(gè)錯(cuò)誤ErrBadPattern。

在filepath包中定義了ErrBadPattern,如下所述:

var ErrBadPattern = errors.New("syntax error in pattern")  

errors.New()用于創(chuàng)建新的錯(cuò)誤。

當(dāng)模式出現(xiàn)錯(cuò)誤時(shí),由Glob函數(shù)返回ErrBadPattern。

讓我們寫一個(gè)小程序來檢查這個(gè)錯(cuò)誤:

package main

import (  
    "fmt"
    "path/filepath"
)

func main() {  
    files, error := filepath.Glob("[")
    if error != nil && error == filepath.ErrBadPattern {
        fmt.Println(error)
        return
    }
    fmt.Println("matched files", files)
}

運(yùn)行結(jié)果:

syntax error in pattern  

不要忽略錯(cuò)誤

永遠(yuǎn)不要忽略一個(gè)錯(cuò)誤。忽視錯(cuò)誤會(huì)招致麻煩。讓我重新編寫一個(gè)示例,該示例列出了與模式匹配的所有文件的名稱,而忽略了錯(cuò)誤處理代碼。

package main

import (  
    "fmt"
    "path/filepath"
)

func main() {  
    files, _ := filepath.Glob("[")
    fmt.Println("matched files", files)
}

我們從前面的例子中已經(jīng)知道模式是無效的。我忽略了Glob函數(shù)返回的錯(cuò)誤,方法是使用行號(hào)中的空白標(biāo)識(shí)符。

matched files []  

由于我們忽略了這個(gè)錯(cuò)誤,輸出看起來好像沒有文件匹配這個(gè)模式,但是實(shí)際上這個(gè)模式本身是畸形的。所以不要忽略錯(cuò)誤。

1.4 自定義錯(cuò)誤

創(chuàng)建自定義錯(cuò)誤可以使用errors包下的New()函數(shù),以及fmt包下的:Errorf()函數(shù)。

//errors包:
func New(text string) error {}

//fmt包:
func Errorf(format string, a ...interface{}) error {}

在使用New()函數(shù)創(chuàng)建自定義錯(cuò)誤之前,讓我們了解它是如何實(shí)現(xiàn)的。下面提供了錯(cuò)誤包中的新功能的實(shí)現(xiàn)。

// Package errors implements functions to manipulate errors.
  package errors

  // New returns an error that formats as the given text.
  func New(text string) error {
      return &errorString{text}
  }

  // errorString is a trivial implementation of error.
  type errorString struct {
      s string
  }

  func (e *errorString) Error() string {
      return e.s
  }

既然我們知道了New()函數(shù)是如何工作的,那么就讓我們?cè)谧约旱某绦蛑惺褂盟鼇韯?chuàng)建一個(gè)自定義錯(cuò)誤。

我們將創(chuàng)建一個(gè)簡單的程序,計(jì)算一個(gè)圓的面積,如果半徑為負(fù),將返回一個(gè)錯(cuò)誤。

package main

import (  
    "errors"
    "fmt"
    "math"
)

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, errors.New("Area calculation failed, radius is less than zero")
    }
    return math.Pi * radius * radius, nil
}

func main() {  
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

運(yùn)行結(jié)果:

Area calculation failed, radius is less than zero 

使用Errorf向錯(cuò)誤添加更多信息

上面的程序運(yùn)行得很好,但是如果我們打印出導(dǎo)致錯(cuò)誤的實(shí)際半徑,那就不好了。這就是fmt包的Errorf函數(shù)的用武之地。這個(gè)函數(shù)根據(jù)一個(gè)格式說明器格式化錯(cuò)誤,并返回一個(gè)字符串作為值來滿足錯(cuò)誤。

使用Errorf函數(shù),修改程序。

package main

import (  
    "fmt"
    "math"
)

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, fmt.Errorf("Area calculation failed, radius %0.2f is less than zero", radius)
    }
    return math.Pi * radius * radius, nil
}

func main() {  
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

運(yùn)行結(jié)果:

Area calculation failed, radius -20.00 is less than zero  

使用struct類型和字段提供關(guān)于錯(cuò)誤的更多信息

還可以使用將錯(cuò)誤接口實(shí)現(xiàn)為錯(cuò)誤的struct類型。這給我們提供了更多的錯(cuò)誤處理的靈活性。在我們的示例中,如果我們想要訪問導(dǎo)致錯(cuò)誤的半徑,那么現(xiàn)在唯一的方法是解析錯(cuò)誤描述區(qū)域計(jì)算失敗,半徑-20.00小于零。這不是一種正確的方法,因?yàn)槿绻枋霭l(fā)生了變化,我們的代碼就會(huì)中斷。

我們將使用在前面的教程中解釋的標(biāo)準(zhǔn)庫的策略,在“斷言底層結(jié)構(gòu)類型并從struct字段獲取更多信息”,并使用struct字段來提供對(duì)導(dǎo)致錯(cuò)誤的半徑的訪問。我們將創(chuàng)建一個(gè)實(shí)現(xiàn)錯(cuò)誤接口的struct類型,并使用它的字段來提供關(guān)于錯(cuò)誤的更多信息。

第一步是創(chuàng)建一個(gè)struct類型來表示錯(cuò)誤。錯(cuò)誤類型的命名約定是,名稱應(yīng)該以文本Error結(jié)束。讓我們把struct類型命名為areaError

type areaError struct {  
    err    string
    radius float64
}

上面的struct類型有一個(gè)字段半徑,它存儲(chǔ)了為錯(cuò)誤負(fù)責(zé)的半徑的值,并且錯(cuò)誤字段存儲(chǔ)了實(shí)際的錯(cuò)誤消息。

下一步,是實(shí)現(xiàn)error 接口

func (e *areaError) Error() string {  
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

在上面的代碼片段中,我們使用一個(gè)指針接收器區(qū)域錯(cuò)誤來實(shí)現(xiàn)錯(cuò)誤接口的Error() string方法。這個(gè)方法打印出半徑和錯(cuò)誤描述。

package main

import (  
    "fmt"
    "math"
)

type areaError struct {  
    err    string
    radius float64
}

func (e *areaError) Error() string {  
    return fmt.Sprintf("radius %0.2f: %s", e.radius, e.err)
}

func circleArea(radius float64) (float64, error) {  
    if radius < 0 {
        return 0, &areaError{"radius is negative", radius}
    }
    return math.Pi * radius * radius, nil
}

func main() {  
    radius := -20.0
    area, err := circleArea(radius)
    if err != nil {
        if err, ok := err.(*areaError); ok {
            fmt.Printf("Radius %0.2f is less than zero", err.radius)
            return
        }
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

程序輸出:

Radius -20.00 is less than zero

使用結(jié)構(gòu)類型的方法提供關(guān)于錯(cuò)誤的更多信息

在本節(jié)中,我們將編寫一個(gè)程序來計(jì)算矩形的面積。如果長度或?qū)挾刃∮?,這個(gè)程序?qū)⑤敵鲆粋€(gè)錯(cuò)誤。

第一步是創(chuàng)建一個(gè)結(jié)構(gòu)來表示錯(cuò)誤。

type areaError struct {  
    err    string //error description
    length float64 //length which caused the error
    width  float64 //width which caused the error
}

上面的錯(cuò)誤結(jié)構(gòu)類型包含一個(gè)錯(cuò)誤描述字段,以及導(dǎo)致錯(cuò)誤的長度和寬度。

現(xiàn)在我們有了錯(cuò)誤類型,讓我們實(shí)現(xiàn)錯(cuò)誤接口,并在錯(cuò)誤類型上添加一些方法來提供關(guān)于錯(cuò)誤的更多信息。

func (e *areaError) Error() string {  
    return e.err
}

func (e *areaError) lengthNegative() bool {  
    return e.length < 0
}

func (e *areaError) widthNegative() bool {  
    return e.width < 0
}

在上面的代碼片段中,我們返回Error() string 方法的錯(cuò)誤描述。當(dāng)長度小于0時(shí),lengthNegative() bool方法返回true;當(dāng)寬度小于0時(shí),widthNegative() bool方法返回true。這兩種方法提供了更多關(guān)于誤差的信息,在這種情況下,他們說面積計(jì)算是否失敗,因?yàn)殚L度是負(fù)的,還是寬度為負(fù)的。因此,我們使用了struct錯(cuò)誤類型的方法來提供更多關(guān)于錯(cuò)誤的信息。

下一步是寫出面積計(jì)算函數(shù)。

func rectArea(length, width float64) (float64, error) {  
    err := ""
    if length < 0 {
        err += "length is less than zero"
    }
    if width < 0 {
        if err == "" {
            err = "width is less than zero"
        } else {
            err += ", width is less than zero"
        }
    }
    if err != "" {
        return 0, &areaError{err, length, width}
    }
    return length * width, nil
}

上面的rectArea函數(shù)檢查長度或?qū)挾仁欠裥∮?,如果它返回一個(gè)錯(cuò)誤消息,則返回矩形的面積為nil。

主函數(shù):

func main() {  
    length, width := -5.0, -9.0
    area, err := rectArea(length, width)
    if err != nil {
        if err, ok := err.(*areaError); ok {
            if err.lengthNegative() {
                fmt.Printf("error: length %0.2f is less than zero\n", err.length)

            }
            if err.widthNegative() {
                fmt.Printf("error: width %0.2f is less than zero\n", err.width)

            }
        }
        fmt.Println(err)
        return
    }
    fmt.Println("area of rect", area)
}

運(yùn)行結(jié)果:

error: length -5.00 is less than zero  
error: width -9.00 is less than zero 

1.5 panic()和recover()

Golang中引入兩個(gè)內(nèi)置函數(shù)panic和recover來觸發(fā)和終止異常處理流程,同時(shí)引入關(guān)鍵字defer來延遲執(zhí)行defer后面的函數(shù)。 一直等到包含defer語句的函數(shù)執(zhí)行完畢時(shí),延遲函數(shù)(defer后的函數(shù))才會(huì)被執(zhí)行,而不管包含defer語句的函數(shù)是通過return的正常結(jié)束,還是由于panic導(dǎo)致的異常結(jié)束。你可以在一個(gè)函數(shù)中執(zhí)行多條defer語句,它們的執(zhí)行順序與聲明順序相反。 當(dāng)程序運(yùn)行時(shí),如果遇到引用空指針、下標(biāo)越界或顯式調(diào)用panic函數(shù)等情況,則先觸發(fā)panic函數(shù)的執(zhí)行,然后調(diào)用延遲函數(shù)。調(diào)用者繼續(xù)傳遞panic,因此該過程一直在調(diào)用棧中重復(fù)發(fā)生:函數(shù)停止執(zhí)行,調(diào)用延遲執(zhí)行函數(shù)等。如果一路在延遲函數(shù)中沒有recover函數(shù)的調(diào)用,則會(huì)到達(dá)該協(xié)程的起點(diǎn),該協(xié)程結(jié)束,然后終止其他所有協(xié)程,包括主協(xié)程(類似于C語言中的主線程,該協(xié)程ID為1)。

panic: 1、內(nèi)建函數(shù) 2、假如函數(shù)F中書寫了panic語句,會(huì)終止其后要執(zhí)行的代碼,在panic所在函數(shù)F內(nèi)如果存在要執(zhí)行的defer函數(shù)列表,按照defer的逆序執(zhí)行 3、返回函數(shù)F的調(diào)用者G,在G中,調(diào)用函數(shù)F語句之后的代碼不會(huì)執(zhí)行,假如函數(shù)G中存在要執(zhí)行的defer函數(shù)列表,按照defer的逆序執(zhí)行,這里的defer 有點(diǎn)類似 try-catch-finally 中的 finally 4、直到goroutine整個(gè)退出,并報(bào)告錯(cuò)誤

recover: 1、內(nèi)建函數(shù) 2、用來控制一個(gè)goroutine的panicking行為,捕獲panic,從而影響應(yīng)用的行為 3、一般的調(diào)用建議 a). 在defer函數(shù)中,通過recever來終止一個(gè)gojroutine的panicking過程,從而恢復(fù)正常代碼的執(zhí)行 b). 可以獲取通過panic傳遞的error

簡單來講:go中可以拋出一個(gè)panic的異常,然后在defer中通過recover捕獲這個(gè)異常,然后正常處理。

錯(cuò)誤和異常從Golang機(jī)制上講,就是error和panic的區(qū)別。很多其他語言也一樣,比如C++/Java,沒有error但有errno,沒有panic但有throw。

Golang錯(cuò)誤和異常是可以互相轉(zhuǎn)換的:

  1. 錯(cuò)誤轉(zhuǎn)異常,比如程序邏輯上嘗試請(qǐng)求某個(gè)URL,最多嘗試三次,嘗試三次的過程中請(qǐng)求失敗是錯(cuò)誤,嘗試完第三次還不成功的話,失敗就被提升為異常了。
  2. 異常轉(zhuǎn)錯(cuò)誤,比如panic觸發(fā)的異常被recover恢復(fù)后,將返回值中error類型的變量進(jìn)行賦值,以便上層函數(shù)繼續(xù)走錯(cuò)誤處理流程。

什么情況下用錯(cuò)誤表達(dá),什么情況下用異常表達(dá),就得有一套規(guī)則,否則很容易出現(xiàn)一切皆錯(cuò)誤或一切皆異常的情況。

以下給出異常處理的作用域(場景):

  1. 空指針引用
  2. 下標(biāo)越界
  3. 除數(shù)為0
  4. 不應(yīng)該出現(xiàn)的分支,比如default
  5. 輸入不應(yīng)該引起函數(shù)錯(cuò)誤

其他場景我們使用錯(cuò)誤處理,這使得我們的函數(shù)接口很精煉。對(duì)于異常,我們可以選擇在一個(gè)合適的上游去recover,并打印堆棧信息,使得部署后的程序不會(huì)終止。

說明: Golang錯(cuò)誤處理方式一直是很多人詬病的地方,有些人吐槽說一半的代碼都是"if err != nil { / 打印 && 錯(cuò)誤處理 / }",嚴(yán)重影響正常的處理邏輯。當(dāng)我們區(qū)分錯(cuò)誤和異常,根據(jù)規(guī)則設(shè)計(jì)函數(shù),就會(huì)大大提高可讀性和可維護(hù)性。

1.6 錯(cuò)誤處理的正確姿勢(shì)

姿勢(shì)一:失敗的原因只有一個(gè)時(shí),不使用error

我們看一個(gè)案例:

func (self *AgentContext) CheckHostType(host_type string) error {
    switch host_type {
    case "virtual_machine":
        return nil
    case "bare_metal":
        return nil
    }
    return errors.New("CheckHostType ERROR:" + host_type)
}

我們可以看出,該函數(shù)失敗的原因只有一個(gè),所以返回值的類型應(yīng)該為bool,而不是error,重構(gòu)一下代碼:

func (self *AgentContext) IsValidHostType(hostType string) bool {
    return hostType == "virtual_machine" || hostType == "bare_metal"
}

說明:大多數(shù)情況,導(dǎo)致失敗的原因不止一種,尤其是對(duì)I/O操作而言,用戶需要了解更多的錯(cuò)誤信息,這時(shí)的返回值類型不再是簡單的bool,而是error。

姿勢(shì)二:沒有失敗時(shí),不使用error

error在Golang中是如此的流行,以至于很多人設(shè)計(jì)函數(shù)時(shí)不管三七二十一都使用error,即使沒有一個(gè)失敗原因。 我們看一下示例代碼:

func (self *CniParam) setTenantId() error {
    self.TenantId = self.PodNs
    return nil
}

對(duì)于上面的函數(shù)設(shè)計(jì),就會(huì)有下面的調(diào)用代碼:

err := self.setTenantId()
if err != nil {
    // log
    // free resource
    return errors.New(...)
}

根據(jù)我們的正確姿勢(shì),重構(gòu)一下代碼:

func (self *CniParam) setTenantId() {
    self.TenantId = self.PodNs
}

于是調(diào)用代碼變?yōu)椋?/p>

self.setTenantId()

姿勢(shì)三:error應(yīng)放在返回值類型列表的最后

對(duì)于返回值類型error,用來傳遞錯(cuò)誤信息,在Golang中通常放在最后一個(gè)。

resp, err := http.Get(url)
if err != nil {
    return nill, err
}

bool作為返回值類型時(shí)也一樣。

value, ok := cache.Lookup(key) 
if !ok {
    // ...cache[key] does not exist… 
}

姿勢(shì)四:錯(cuò)誤值統(tǒng)一定義,而不是跟著感覺走

很多人寫代碼時(shí),到處return errors.New(value),而錯(cuò)誤value在表達(dá)同一個(gè)含義時(shí)也可能形式不同,比如“記錄不存在”的錯(cuò)誤value可能為:

  1. "record is not existed."
  2. "record is not exist!"
  3. "###record is not existed?。?!"
  4. ...

這使得相同的錯(cuò)誤value撒在一大片代碼里,當(dāng)上層函數(shù)要對(duì)特定錯(cuò)誤value進(jìn)行統(tǒng)一處理時(shí),需要漫游所有下層代碼,以保證錯(cuò)誤value統(tǒng)一,不幸的是有時(shí)會(huì)有漏網(wǎng)之魚,而且這種方式嚴(yán)重阻礙了錯(cuò)誤value的重構(gòu)。

于是,我們可以參考C/C++的錯(cuò)誤碼定義文件,在Golang的每個(gè)包中增加一個(gè)錯(cuò)誤對(duì)象定義文件,如下所示:

var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")

姿勢(shì)五:錯(cuò)誤逐層傳遞時(shí),層層都加日志

層層都加日志非常方便故障定位。

說明:至于通過測(cè)試來發(fā)現(xiàn)故障,而不是日志,目前很多團(tuán)隊(duì)還很難做到。如果你或你的團(tuán)隊(duì)能做到,那么請(qǐng)忽略這個(gè)姿勢(shì)。

姿勢(shì)六:錯(cuò)誤處理使用defer

我們一般通過判斷error的值來處理錯(cuò)誤,如果當(dāng)前操作失敗,需要將本函數(shù)中已經(jīng)create的資源destroy掉,示例代碼如下:

func deferDemo() error {
    err := createResource1()
    if err != nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    err = createResource2()
    if err != nil {
        destroyResource1()
        return ERR_CREATE_RESOURCE2_FAILED
    }

    err = createResource3()
    if err != nil {
        destroyResource1()
        destroyResource2()
        return ERR_CREATE_RESOURCE3_FAILED
    }

    err = createResource4()
    if err != nil {
        destroyResource1()
        destroyResource2()
        destroyResource3()
        return ERR_CREATE_RESOURCE4_FAILED
    } 
    return nil
}

當(dāng)Golang的代碼執(zhí)行時(shí),如果遇到defer的閉包調(diào)用,則壓入堆棧。當(dāng)函數(shù)返回時(shí),會(huì)按照后進(jìn)先出的順序調(diào)用閉包。 對(duì)于閉包的參數(shù)是值傳遞,而對(duì)于外部變量卻是引用傳遞,所以閉包中的外部變量err的值就變成外部函數(shù)返回時(shí)最新的err值。 根據(jù)這個(gè)結(jié)論,我們重構(gòu)上面的示例代碼:

func deferDemo() error {
    err := createResource1()
    if err != nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource1()
        }
    }()
    err = createResource2()
    if err != nil {
        return ERR_CREATE_RESOURCE2_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource2()
                   }
    }()

    err = createResource3()
    if err != nil {
        return ERR_CREATE_RESOURCE3_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource3()
        }
    }()

    err = createResource4()
    if err != nil {
        return ERR_CREATE_RESOURCE4_FAILED
    }
    return nil
}

姿勢(shì)七:當(dāng)嘗試幾次可以避免失敗時(shí),不要立即返回錯(cuò)誤

如果錯(cuò)誤的發(fā)生是偶然性的,或由不可預(yù)知的問題導(dǎo)致。一個(gè)明智的選擇是重新嘗試失敗的操作,有時(shí)第二次或第三次嘗試時(shí)會(huì)成功。在重試時(shí),我們需要限制重試的時(shí)間間隔或重試的次數(shù),防止無限制的重試。

兩個(gè)案例:

  1. 我們平時(shí)上網(wǎng)時(shí),嘗試請(qǐng)求某個(gè)URL,有時(shí)第一次沒有響應(yīng),當(dāng)我們?cè)俅嗡⑿聲r(shí),就有了驚喜。
  2. 團(tuán)隊(duì)的一個(gè)QA曾經(jīng)建議當(dāng)Neutron的attach操作失敗時(shí),最好嘗試三次,這在當(dāng)時(shí)的環(huán)境下驗(yàn)證果然是有效的。

姿勢(shì)八:當(dāng)上層函數(shù)不關(guān)心錯(cuò)誤時(shí),建議不返回error

對(duì)于一些資源清理相關(guān)的函數(shù)(destroy/delete/clear),如果子函數(shù)出錯(cuò),打印日志即可,而無需將錯(cuò)誤進(jìn)一步反饋到上層函數(shù),因?yàn)橐话闱闆r下,上層函數(shù)是不關(guān)心執(zhí)行結(jié)果的,或者即使關(guān)心也無能為力,于是我們建議將相關(guān)函數(shù)設(shè)計(jì)為不返回error。

姿勢(shì)九:當(dāng)發(fā)生錯(cuò)誤時(shí),不忽略有用的返回值

通常,當(dāng)函數(shù)返回non-nil的error時(shí),其他的返回值是未定義的(undefined),這些未定義的返回值應(yīng)該被忽略。然而,有少部分函數(shù)在發(fā)生錯(cuò)誤時(shí),仍然會(huì)返回一些有用的返回值。比如,當(dāng)讀取文件發(fā)生錯(cuò)誤時(shí),Read函數(shù)會(huì)返回可以讀取的字節(jié)數(shù)以及錯(cuò)誤信息。對(duì)于這種情況,應(yīng)該將讀取到的字符串和錯(cuò)誤信息一起打印出來。

說明:對(duì)函數(shù)的返回值要有清晰的說明,以便于其他人使用。

1.7 異常處理的正確姿勢(shì)

姿勢(shì)一:在程序開發(fā)階段,堅(jiān)持速錯(cuò)

速錯(cuò),簡單來講就是“讓它掛”,只有掛了你才會(huì)第一時(shí)間知道錯(cuò)誤。在早期開發(fā)以及任何發(fā)布階段之前,最簡單的同時(shí)也可能是最好的方法是調(diào)用panic函數(shù)來中斷程序的執(zhí)行以強(qiáng)制發(fā)生錯(cuò)誤,使得該錯(cuò)誤不會(huì)被忽略,因而能夠被盡快修復(fù)。

姿勢(shì)二:在程序部署后,應(yīng)恢復(fù)異常避免程序終止

在Golang中,某個(gè)Goroutine如果panic了,并且沒有recover,那么整個(gè)Golang進(jìn)程就會(huì)異常退出。所以,一旦Golang程序部署后,在任何情況下發(fā)生的異常都不應(yīng)該導(dǎo)致程序異常退出,我們?cè)谏蠈雍瘮?shù)中加一個(gè)延遲執(zhí)行的recover調(diào)用來達(dá)到這個(gè)目的,并且是否進(jìn)行recover需要根據(jù)環(huán)境變量或配置文件來定,默認(rèn)需要recover。 這個(gè)姿勢(shì)類似于C語言中的斷言,但還是有區(qū)別:一般在Release版本中,斷言被定義為空而失效,但需要有if校驗(yàn)存在進(jìn)行異常保護(hù),盡管契約式設(shè)計(jì)中不建議這樣做。在Golang中,recover完全可以終止異常展開過程,省時(shí)省力。

我們?cè)谡{(diào)用recover的延遲函數(shù)中以最合理的方式響應(yīng)該異常:

  1. 打印堆棧的異常調(diào)用信息和關(guān)鍵的業(yè)務(wù)信息,以便這些問題保留可見;
  2. 將異常轉(zhuǎn)換為錯(cuò)誤,以便調(diào)用者讓程序恢復(fù)到健康狀態(tài)并繼續(xù)安全運(yùn)行。

我們看一個(gè)簡單的例子:

func funcA() error {
    defer func() {
        if p := recover(); p != nil {
            fmt.Printf("panic recover! p: %v", p)
            debug.PrintStack()
        }
    }()
    return funcB()
}

func funcB() error {
    // simulation
    panic("foo")
    return errors.New("success")
}

func test() {
    err := funcA()
    if err == nil {
        fmt.Printf("err is nil\\n")
    } else {
        fmt.Printf("err is %v\\n", err)
    }
}

我們期望test函數(shù)的輸出是:

err is foo

實(shí)際上test函數(shù)的輸出是:

err is nil

原因是panic異常處理機(jī)制不會(huì)自動(dòng)將錯(cuò)誤信息傳遞給error,所以要在funcA函數(shù)中進(jìn)行顯式的傳遞,代碼如下所示:

func funcA() (err error) {
    defer func() {
        if p := recover(); p != nil {
            fmt.Println("panic recover! p:", p)
            str, ok := p.(string)
            if ok {
                err = errors.New(str)
            } else {
                err = errors.New("panic")
            }
            debug.PrintStack()
        }
    }()
    return funcB()
}

姿勢(shì)三:對(duì)于不應(yīng)該出現(xiàn)的分支,使用異常處理

當(dāng)某些不應(yīng)該發(fā)生的場景發(fā)生時(shí),我們就應(yīng)該調(diào)用panic函數(shù)來觸發(fā)異常。比如,當(dāng)程序到達(dá)了某條邏輯上不可能到達(dá)的路徑:

switch s := suit(drawCard()); s {
    case "Spades":
    // ...
    case "Hearts":
    // ...
    case "Diamonds":
    // ... 
    case "Clubs":
    // ...
    default:
        panic(fmt.Sprintf("invalid suit %v", s))
}

姿勢(shì)四:針對(duì)入?yún)⒉粦?yīng)該有問題的函數(shù),使用panic設(shè)計(jì)

入?yún)⒉粦?yīng)該有問題一般指的是硬編碼,我們先看這兩個(gè)函數(shù)(Compile和MustCompile),其中MustCompile函數(shù)是對(duì)Compile函數(shù)的包裝:

func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    if error != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
    }
    return regexp
}

所以,對(duì)于同時(shí)支持用戶輸入場景和硬編碼場景的情況,一般支持硬編碼場景的函數(shù)是對(duì)支持用戶輸入場景函數(shù)的包裝。 對(duì)于只支持硬編碼單一場景的情況,函數(shù)設(shè)計(jì)時(shí)直接使用panic,即返回值類型列表中不會(huì)有error,這使得函數(shù)的調(diào)用處理非常方便(沒有了乏味的"if err != nil {/ 打印 && 錯(cuò)誤處理 /}"代碼塊)。

?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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