<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          GO 編程模式系列(二):錯誤處理

          共 7869字,需瀏覽 16分鐘

           ·

          2020-12-30 21:10

          原文作者陳皓(左耳朵耗子)

          內(nèi)容出

          https://coolshell.cn/articles/21128.html




          錯誤處理一直以一是編程必需要面對的問題,錯誤處理如果做的好的話,代碼的穩(wěn)定性會很好。不同的語言有不同的出現(xiàn)處理的方式。Go語言也一樣,在本篇文章中,我們來討論一下Go語言的出錯出處,尤其是那令人抓狂的?if err != nil?。
          在正式討論Go代碼里滿屏的?if err != nil?怎么辦這個事之前,我想先說一說編程中的錯誤處理。這樣可以讓大家在更高的層面理解編程中的錯誤處理。

          本文是全系列中第2 / 9篇:Go編程模式

          • Go編程模式:切片,接口,時間和性能
          • Go 編程模式:錯誤處理
          • Go 編程模式:Functional Options
          • Go編程模式:委托和反轉(zhuǎn)控制
          • Go編程模式:Map-Reduce
          • Go 編程模式:Go Generation
          • Go編程模式:修飾器
          • Go編程模式:Pipeline
          • Go 編程模式:k8s Visitor 模式

          目錄

          C語言的錯誤檢查

          首先,我們知道,處理錯誤最直接的方式是通過錯誤碼,這也是傳統(tǒng)的方式,在過程式語言中通常都是用這樣的方式處理錯誤的。比如 C 語言,基本上來說,其通過函數(shù)的返回值標識是否有錯,然后通過全局的?errno?變量并配合一個?errstr?的數(shù)組來告訴你為什么出錯。
          為什么是這樣的設計?道理很簡單,除了可以共用一些錯誤,更重要的是這其實是一種妥協(xié)。比如:read(),?write(),?open()?這些函數(shù)的返回值其實是返回有業(yè)務邏輯的值。也就是說,這些函數(shù)的返回值有兩種語義,一種是成功的值,比如?open()?返回的文件句柄指針?FILE*?,或是錯誤?NULL。這樣會導致調(diào)用者并不知道是什么原因出錯了,需要去檢查?errno?來獲得出錯的原因,從而可以正確地處理錯誤。
          一般而言,這樣的錯誤處理方式在大多數(shù)情況下是沒什么問題的。但是也有例外的情況,我們來看一下下面這個 C 語言的函數(shù):
          int atoi(const char *str)
          這個函數(shù)是把一個字符串轉(zhuǎn)成整型。但是問題來了,如果一個要傳的字符串是非法的(不是數(shù)字的格式),如 “ABC” 或者整型溢出了,那么這個函數(shù)應該返回什么呢?出錯返回,返回什么數(shù)都不合理,因為這會和正常的結(jié)果混淆在一起。比如,返回?0,那么會和正常的對 “0” 字符的返回值完全混淆在一起。這樣就無法判斷出錯的情況。你可能會說,是不是要檢查一下?errno,按道理說應該是要去檢查的,但是,我們在 C99 的規(guī)格說明書中可以看到這樣的描述——
          7.20.1The functions atof, atoi, atol, and atoll need not affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is unde?ned.
          atoi(),?atof(),?atol()?或是?atoll()?這樣的函數(shù)是不會設置?errno的,而且,還說了,如果結(jié)果無法計算的話,行為是undefined。所以,后來,libc 又給出了一個新的函數(shù)strtol(),這個函數(shù)在出錯的時會設置全局變量?errno?:
          long val = strtol(in_str, &endptr, 10);  //10的意思是10進制//如果無法轉(zhuǎn)換if (endptr == str) {    fprintf(stderr, "No digits were found\n");    exit(EXIT_FAILURE);}//如果整型溢出了if ((errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)) {    fprintf(stderr, "ERROR: number out of range for LONG\n");    exit(EXIT_FAILURE); }//如果是其它錯誤if (errno != 0 && val == 0) {    perror("strtol");    exit(EXIT_FAILURE);}
          雖然,strtol()?函數(shù)解決了?atoi()?函數(shù)的問題,但是我們還是能感覺到不是很舒服和自然。
          因為,這種用 返回值 + errno 的錯誤檢查方式會有一些問題:
          • 程序員一不小心就會忘記返回值的檢查,從而造成代碼的 Bug;
          • 函數(shù)接口非常不純潔,正常值和錯誤值混淆在一起,導致語義有問題。
          所以,后來,有一些類庫就開始區(qū)分這樣的事情。比如,Windows 的系統(tǒng)調(diào)用開始使用?HRESULT?的返回來統(tǒng)一錯誤的返回值,這樣可以明確函數(shù)調(diào)用時的返回值是成功還是錯誤。但這樣一來,函數(shù)的 input 和 output 只能通過函數(shù)的參數(shù)來完成,于是出現(xiàn)了所謂的 入?yún)?和 出參 這樣的區(qū)別。
          然而,這又使得函數(shù)接入中參數(shù)的語義變得復雜,一些參數(shù)是入?yún)?,一些參?shù)是出參,函數(shù)接口變得復雜了一些。而且,依然沒有解決函數(shù)的成功或失敗可以被人為忽略的問題。

          Java的錯誤處理

          Java語言使用?try-catch-finally?通過使用異常的方式來處理錯誤,其實,這比起C語言的錯處理進了一大步,使用拋異常和抓異常的方式可以讓我們的代碼有這樣的一些好處:
          • 函數(shù)接口在 input(參數(shù))和 output(返回值)以及錯誤處理的語義是比較清楚的。
          • 正常邏輯的代碼可以與錯誤處理和資源清理的代碼分開,提高了代碼的可讀性。
          • 異常不能被忽略(如果要忽略也需要 catch 住,這是顯式忽略)。
          • 在面向?qū)ο蟮恼Z言中(如 Java),異常是個對象,所以,可以實現(xiàn)多態(tài)式的 catch。
          • 與狀態(tài)返回碼相比,異常捕捉有一個顯著的好處是,函數(shù)可以嵌套調(diào)用,或是鏈式調(diào)用。比如:
            • int x = add(a, div(b,c));
            • Pizza p = PizzaBuilder().SetSize(sz).SetPrice(p)...;

          Go語言的錯誤處理

          Go 語言的函數(shù)支持多返回值,所以,可以在返回接口把業(yè)務語義(業(yè)務返回值)和控制語義(出錯返回值)區(qū)分開來。Go 語言的很多函數(shù)都會返回 result, err 兩個值,于是:
          • 參數(shù)上基本上就是入?yún)?,而返回接口把結(jié)果和錯誤分離,這樣使得函數(shù)的接口語義清晰;
          • 而且,Go 語言中的錯誤參數(shù)如果要忽略,需要顯式地忽略,用 _ 這樣的變量來忽略;
          • 另外,因為返回的?error?是個接口(其中只有一個方法?Error(),返回一個?string?),所以你可以擴展自定義的錯誤處理。
          另外,如果一個函數(shù)返回了多個不同類型的?error,你也可以使用下面這樣的方式:
          if err != nil {  switch err.(type) {    case *json.SyntaxError:      ...    case *ZeroDivisionError:      ...    case *NullPointerError:      ...    default:      ...  }}
          我們可以看到,Go語言的錯誤處理的的方式,本質(zhì)上是返回值檢查,但是他也兼顧了異常的一些好處 – 對錯誤的擴展。

          資源清理

          出錯后是需要做資源清理的,不同的編程語言有不同的資源清理的編程模式:
          • C語言 – 使用的是?goto fail;?的方式到一個集中的地方進行清理(有篇有意思的文章可以看一下《由蘋果的低級BUG想到的》)
          • C++語言- 一般來說使用 RAII模式,通過面向?qū)ο蟮拇砟J?,把需要清理的資源交給一個代理類,然后在析構(gòu)函數(shù)來解決。
          • Java語言 – 可以在finally 語句塊里進行清理。
          • Go語言 – 使用?defer?關鍵詞進行清理。
          下面是一個Go語言的資源清理的示例:
          func Close(c io.Closer) {  err := c.Close()  if err != nil {    log.Fatal(err)  }}func main() {  r, err := Open("a")  if err != nil {    log.Fatalf("error opening 'a'\n")  }  defer Close(r) // 使用defer關鍵字在函數(shù)退出時關閉文件。  r, err = Open("b")  if err != nil {    log.Fatalf("error opening 'b'\n")  }  defer Close(r) // 使用defer關鍵字在函數(shù)退出時關閉文件。}

          Error Check? Hell

          好了,說到 Go 語言的?if err !=nil?的代碼了,這樣的代碼的確是能讓人寫到吐。那么有沒有什么好的方式呢,有的。我們先看如下的一個令人崩潰的代碼。
          func parse(r io.Reader) (*Point, error) {    var p Point    if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {        return nil, err    }    if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {        return nil, err    }    if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {        return nil, err    }    if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {        return nil, err    }    if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {        return nil, err    }}
          要解決這個事,我們可以用函數(shù)式編程的方式,如下代碼示例:
          func parse(r io.Reader) (*Point, error) {    var p Point    var err error    read := func(data interface{}) {        if err != nil {            return        }        err = binary.Read(r, binary.BigEndian, data)    }    read(&p.Longitude)    read(&p.Latitude)    read(&p.Distance)    read(&p.ElevationGain)    read(&p.ElevationLoss)    if err != nil {        return &p, err    }    return &p, nil}
          上面的代碼我們可以看到,我們通過使用Closure 的方式把相同的代碼給抽出來重新定義一個函數(shù),這樣大量的??if err!=nil?處理的很干凈了。但是會帶來一個問題,那就是有一個?err?變量和一個內(nèi)部的函數(shù),感覺不是很干凈。
          那么,我們還能不能搞得更干凈一點呢,我們從Go 語言的?bufio.Scanner()中似乎可以學習到一些東西:
          scanner := bufio.NewScanner(input)for scanner.Scan() {    token := scanner.Text()    // process token}if err := scanner.Err(); err != nil {    // process the error}
          上面的代碼我們可以看到,scanner在操作底層的I/O的時候,那個for-loop中沒有任何的?if err !=nil?的情況,退出循環(huán)后有一個?scanner.Err()?的檢查??磥硎褂昧私Y(jié)構(gòu)體的方式。模仿它,我們可以把我們的代碼重構(gòu)成下面這樣:
          首先,定義一個結(jié)構(gòu)體和一個成員函數(shù):
          type Reader struct {    r   io.Reader    err error}func (r *Reader) read(data interface{}) {    if r.err == nil {        r.err = binary.Read(r.r, binary.BigEndian, data)    }}
          然后,我們的代碼就可以變成下面這樣:
          func parse(input io.Reader) (*Point, error) {    var p Point    r := Reader{r: input}    r.read(&p.Longitude)    r.read(&p.Latitude)    r.read(&p.Distance)    r.read(&p.ElevationGain)    r.read(&p.ElevationLoss)    if r.err != nil {        return nil, r.err    }    return &p, nil}
          有了上面這個技術,我們的“流式接口 Fluent Interface”,也就很容易處理了。如下所示:
          package mainimport (  "bytes"  "encoding/binary"  "fmt")// 長度不夠,少一個Weightvar b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c} var r = bytes.NewReader(b)type Person struct {  Name [10]byte  Age uint8  Weight uint8  err error}func (p *Person) read(data interface{}) {  if p.err == nil {    p.err = binary.Read(r, binary.BigEndian, data)  }}func (p *Person) ReadName() *Person {  p.read(&p.Name)   return p}func (p *Person) ReadAge() *Person {  p.read(&p.Age)   return p}func (p *Person) ReadWeight() *Person {  p.read(&p.Weight)   return p}func (p *Person) Print() *Person {  if p.err == nil {    fmt.Printf("Name=%s, Age=%d, Weight=%d\n",p.Name, p.Age, p.Weight)  }  return p}func main() {     p := Person{}  p.ReadName().ReadAge().ReadWeight().Print()  fmt.Println(p.err)  // EOF 錯誤}
          相信你應該看懂這個技巧了,但是,其使用場景也就只能在對于同一個業(yè)務對象的不斷操作下可以簡化錯誤處理,對于多個業(yè)務對象的話,還是得需要各種?if err != nil的方式。

          包裝錯誤

          最后,多說一句,我們需要包裝一下錯誤,而不是干巴巴地把err給返回到上層,我們需要把一些執(zhí)行的上下文加入。
          通常來說,我們會使用?fmt.Errorf()來完成這個事,比如:
          if err != nil {   return fmt.Errorf("something failed: %v", err)}
          另外,在Go語言的開發(fā)者中,更為普遍的做法是將錯誤包裝在另一個錯誤中,同時保留原始內(nèi)容:
          type authorizationError struct {    operation string    err error   // original error}func (e *authorizationError) Error() string {    return fmt.Sprintf("authorization failed during %s: %v", e.operation, e.err)}
          當然,更好的方式是通過一種標準的訪問方法,這樣,我們最好使用一個接口,比如?causer接口中實現(xiàn)?Cause()?方法來暴露原始錯誤,以供進一步檢查:
          type causer interface {    Cause() error}func (e *authorizationError) Cause() error {    return e.err}
          這里有個好消息是,這樣的代碼不必再寫了,有一個第三方的錯誤庫(github.com/pkg/errors),對于這個庫,我無論到哪都能看到他的存在,所以,這個基本上來說就是事實上的標準了。代碼示例如下:
          import"github.com/pkg/errors"//錯誤包裝if err != nil{   return errors.Wrap(err, "read failed")}// Cause接口switch err := errors.Cause(err).(type){case *MyError:  // handle specificallydefault:  // unknown error}

          參考文章

          • Golang Error Handling lesson by Rob Pike
            http://jxck.hatenablog.com/entry/golang-error-handling-lesson-by-rob-pike
          • Errors are values
            https://blog.golang.org/errors-are-values
          (全文完)

          瀏覽 28
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91视频盛宴 | 蘑菇视频91 | 天天日天天摸 | 被操逼视频| 狠狠人妻久久久 |