1. 程式人生 > >Go學習(13):異常

Go學習(13):異常

錯誤處理

1.1 什麼是錯誤

錯誤是什麼?

錯誤指出程式中的異常情況。假設我們正在嘗試開啟一個檔案,檔案系統中不存在這個檔案。這是一個異常情況,它表示為一個錯誤。

Go中的錯誤也是一種型別。錯誤用內建的error 型別表示。就像其他型別的,如int,float64。錯誤值可以儲存在變數中,從函式中返回,等等。

1.2 演示錯誤

讓我們從一個示例程式開始,這個程式嘗試開啟一個不存在的檔案。

示例程式碼:

package main

import (  
    "fmt"
    "os"
)

func main() {  
    f, err :=
os.Open("/test.txt") if err != nil { fmt.Println(err) return } //根據f進行檔案的讀或寫 fmt.Println(f.Name(), "opened successfully") }

在os包中有開啟檔案的功能函式:

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

如果檔案已經成功開啟,那麼Open函式將返回檔案處理。如果在開啟檔案時出現錯誤,將返回一個非nil錯誤。

如果一個函式或方法返回一個錯誤,那麼按照慣例,它必須是函式返回的最後一個值。因此,Open

函式返回的值是最後一個值。

處理錯誤的慣用方法是將返回的錯誤與nil進行比較。nil值表示沒有發生錯誤,而非nil值表示出現錯誤。在我們的例子中,我們檢查錯誤是否為nil。如果它不是nil,我們只需列印錯誤並從主函式返回。

執行結果:

open /test.txt: No such file or directory

我們得到一個錯誤,說明該檔案不存在。

1.3 錯誤型別表示

Go 語言通過內建的錯誤介面提供了非常簡單的錯誤處理機制。

讓我們再深入一點,看看如何定義錯誤型別的構建。錯誤是一個帶有以下定義的介面型別,

type error interface {
Error() string }

它包含一個帶有Error()字串的方法。任何實現這個介面的型別都可以作為一個錯誤使用。這個方法提供了對錯誤的描述。

當列印錯誤時,fmt.Println函式在內部呼叫Error() 方法來獲取錯誤的描述。這就是錯誤描述是如何在一行中打印出來的。

從錯誤中提取更多資訊的不同方法

既然我們知道錯誤是一種介面型別,那麼讓我們看看如何提取更多關於錯誤的資訊。

在上面的例子中,我們僅僅是列印了錯誤的描述。如果我們想要的是導致錯誤的檔案的實際路徑。一種可能的方法是解析錯誤字串。這是我們程式的輸出,

open /test.txt: No such file or directory  

我們可以解析這個錯誤訊息並從中獲取檔案路徑"/test.txt"。但這是一個糟糕的方法。在新版本的語言中,錯誤描述可以隨時更改,我們的程式碼將會中斷。

是否有辦法可靠地獲取檔名?答案是肯定的,它可以做到,標準Go庫使用不同的方式提供更多關於錯誤的資訊。讓我們一看一看。

1.斷言底層結構型別並從結構欄位獲取更多資訊

如果仔細閱讀開啟函式的文件,可以看到它返回的是PathError型別的錯誤。PathError是一個struct型別,它在標準庫中的實現如下,

type PathError struct {  
    Op   string
    Path string
    Err  error
}

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

從上面的程式碼中,您可以理解PathError通過宣告錯誤()string方法實現了錯誤介面。該方法連線操作、路徑和實際錯誤並返回它。這樣我們就得到了錯誤資訊,

open /test.txt: No such file or directory 

PathError結構的路徑欄位包含導致錯誤的檔案的路徑。讓我們修改上面寫的程式,並打印出路徑。

修改程式碼:

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")
}

在上面的程式中,我們使用型別斷言獲得錯誤介面的基本值。然後我們用錯誤來列印路徑.這個程式輸出,

File at path /test.txt failed to open  

2. 斷言底層結構型別,並使用方法獲取更多資訊

獲得更多資訊的第二種方法是斷言底層型別,並通過呼叫struct型別的方法獲取更多資訊。

示例程式碼:

type DNSError struct {  
    ...
}

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

從上面的程式碼中可以看到,DNSError struct有兩個方法Timeout() bool和Temporary() bool,它們返回一個布林值,表示錯誤是由於超時還是臨時的。

讓我們編寫一個斷言*DNSError型別的程式,並呼叫這些方法來確定錯誤是臨時的還是超時的。

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)
}

在上面的程式中,我們正在嘗試獲取一個無效域名的ip地址,這是一個無效的域名。golangbot123.com。我們通過宣告它來輸入*net.DNSError來獲得錯誤的潛在價值。

在我們的例子中,錯誤既不是暫時的,也不是由於超時,因此程式會打印出來,

generic error:  lookup golangbot123.com: no such host  

如果錯誤是臨時的或超時的,那麼相應的If語句就會執行,我們可以適當地處理它。

3.直接比較

獲得更多關於錯誤的詳細資訊的第三種方法是直接與型別錯誤的變數進行比較。讓我們通過一個例子來理解這個問題。

filepath包的Glob函式用於返回與模式匹配的所有檔案的名稱。當模式出現錯誤時,該函式將返回一個錯誤ErrBadPattern。

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

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

errors.New()用於建立新的錯誤。

當模式出現錯誤時,由Glob函式返回ErrBadPattern。

讓我們寫一個小程式來檢查這個錯誤:

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)
}

執行結果:

syntax error in pattern  

不要忽略錯誤

永遠不要忽略一個錯誤。忽視錯誤會招致麻煩。讓我重新編寫一個示例,該示例列出了與模式匹配的所有檔案的名稱,而忽略了錯誤處理程式碼。

package main

import (  
    "fmt"
    "path/filepath"
)

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

我們從前面的例子中已經知道模式是無效的。我忽略了Glob函式返回的錯誤,方法是使用行號中的空白識別符號。

matched files []  

由於我們忽略了這個錯誤,輸出看起來好像沒有檔案匹配這個模式,但是實際上這個模式本身是畸形的。所以不要忽略錯誤。

1.4 自定義錯誤

建立自定義錯誤的最簡單方法是使用錯誤包的新功能。

在使用新函式建立自定義錯誤之前,讓我們瞭解它是如何實現的。下面提供了錯誤包中的新功能的實現。

// 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
  }

既然我們知道了新函式是如何工作的,那麼就讓我們在自己的程式中使用它來建立一個自定義錯誤。

我們將建立一個簡單的程式,計算一個圓的面積,如果半徑為負,將返回一個錯誤。

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() {  
	//型別轉換  java 類似  int a = (double) 3.14
	//go 類似  radius := float64(-10)
    radius := -20.0
    area, err := circleArea(radius)
    //nil值表示沒有發生錯誤,而非nil值表示出現錯誤
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("Area of circle %0.2f", area)
}

執行結果:

Area calculation failed, radius is less than zero 
使用Errorf向錯誤新增更多資訊

上面的程式執行得很好,但是如果我們打印出導致錯誤的實際半徑,那就不好了。這就是fmt包的Errorf函式的用武之地。這個函式根據一個格式說明器格式化錯誤,並返回一個字串作為值來滿足錯誤。

使用Errorf函式,修改程式。

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)
}

執行結果:

Area calculation failed, radius -20.00 is less than zero  
使用struct型別和欄位提供關於錯誤的更多資訊

還可以使用將錯誤介面實現為錯誤的struct型別。這給我們提供了更多的錯誤處理的靈活性。在我們的示例中,如果我們想要訪問導致錯誤的半徑,那麼現在唯一的方法是解析錯誤描述區域計算失敗,半徑-20.00小於零。這不是一種正確的方法,因為如果描述發生了變化,我們的程式碼就會中斷。

我們將使用在前面的教程中解釋的標準庫的策略,在“斷言底層結構型別並從struct欄位獲取更多資訊”,並使用struct欄位來提供對導致錯誤的半徑的訪問。我們將建立一個實現錯誤介面的struct型別,並使用它的欄位來提供關於錯誤的更多資訊。

第一步是建立一個struct型別來表示錯誤。錯誤型別的命名約定是,名稱應該以文字Error結束。讓我們把struct型別命名為areaError

type areaError struct {  
    err    string
    radius float64
}

上面的struct型別有一個欄位半徑,它儲存了為錯誤負責的半徑的值,並且錯誤欄位儲存了實際的錯誤訊息。

下一步,是實現error 介面

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

在上面的程式碼片段中,我們使用一個指標接收器區域錯誤來實現錯誤介面的Error() string方法。這個方法打印出半徑和錯誤描述。

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 rectangle1 %0.2f", area)
}

程式輸出:

Radius -20.00 is less than zero
使用結構型別的方法提供關於錯誤的更多資訊

在本節中,我們將編寫一個程式來計算矩形的面積。如果長度或寬度小於0,這個程式將輸出一個錯誤。

第一步是建立一個結構來表示錯誤。

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

上面的錯誤結構型別包含一個錯誤描述欄位,以及導致錯誤的長度和寬度。

現在我們有了錯誤型別,讓我們實現錯誤介面,並在錯誤型別上新增一些方法來提供關於錯誤的更多資訊。

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 方法的錯誤描述。當長度小於0時,lengthNegative() bool方法返回true;當寬度小於0時,widthNegative() bool方法返回true。這兩種方法提供了更多關於誤差的資訊,在這種情況下,他們說面積計算是否失敗,因為長度是負的,還是寬度為負的。因此,我們使用了struct錯誤型別的方法來提供更多關於錯誤的資訊。

下一步是寫出面積計算函式。

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函式檢查長度或寬度是否小於0,如果它返回一個錯誤訊息,則返回矩形的面積為nil。

主函式:

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)

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

執行結果:

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