Go語言進階之路(四):標準錯誤和異常

標準錯誤

Go語言內置的error接口,自定義的類型,只要實現該接口方法即可稱為標準錯誤類型,來看看源碼:

<code>// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
  Error() string
}/<code>

自定義一個錯誤類型,實現error接口的Error()方法:

<code>type MyError struct {
  s string
}
func (myError MyError) Error() string {
  return "MyError"
}
func main() {
  var e MyError = MyError{"err"}
  fmt.Println(e)  // 輸出:MyError
  err, ok := interface{}(e).(error)
  fmt.Println(ok)  // 輸出:true
  fmt.Println(err)  // 輸出:MyError
}/<code>

從上面例子來看,err, ok := interface{}(e).(error)中的類型轉換結果為true,說明自定義的類型就是error的子類型,也就證實了只要某個類型實現了Error()方法,那這個類型就是error類型。

errors包

Go語言的errors包有個內置的錯誤類型叫errorString,來看一下源碼:

<code>package errors


// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
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
}/<code>

從上面的源碼看到,errorString類型實現了error接口的Error()方法,因此errorString類型就是error的子類型。

我們知道,當某個包下面的變量和方法名以小寫開頭時,這個變量或者方法只能在這個包下面才能訪問。因此,我們自己寫的代碼沒辦法直接創建errorString的實例,我們可以使用errors裡的New方法方便的創建error類型。

<code>var e = errors.New("MyError")
fmt.Println(e)  // 輸出:MyError
err, ok := interface{}(e).(error)
fmt.Println(ok)  // 輸出:true
fmt.Println(err)  // 輸出:MyError/<code>

和最上面的例子一樣,err, ok := interface{}(e).(error)中的類型轉換結果為true,說明errorString就是error的子類型。

創建標準錯誤

<code>func f1(arg int) (int, error) { 

    if arg == 42 {
        return -1, errors.New("can't work with 42")  // 使用錯誤提示創建標準錯誤
    }
    return arg + 3, nil
}


type argError struct {
    arg  int
    prob string
}


func (e *argError) Error() string {  // 自定義錯誤類型需要實現Error函數
    return fmt.Sprintf("%d - %s", e.arg, e.prob)
}
func f2(arg int) (int, error) {
    if arg == 42 {
        return -1, &argError{arg, "can't work with it"}
    }
    return arg + 3, nil
}
func main() {
    for _, i := range []int{7, 42} {
        if r, e := f1(i); e != nil {
            fmt.Println("f1 failed:", e)  // f1 failed: can't work with 42
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
    for _, i := range []int{7, 42} {
        if r, e := f2(i); e != nil {
            fmt.Println("f2 failed:", e)
        } else {
            fmt.Println("f2 worked:", r)
        }
    }
    _, e := f2(42)
    if ae, ok := e.(*argError); ok {
        fmt.Println(ae.arg)
        fmt.Println(ae.prob)
    }
}
/<code>

格式化消息的標準錯誤類型

fmt包中有個Errorf方法,我們可以通過它創建一個具有格式化字符串的標準錯誤類型,來看一下fmt.Errorf的源碼:

<code>func Errorf(format string, a ...interface{}) error {
  p := newPrinter()
  p.wrapErrs = true
  p.doPrintf(format, a)
  s := string(p.buf)
  var err error
  if p.wrappedErr == nil {
    err = errors.New(s)
  } else {
    err = &wrapError{s, p.wrappedErr}
  }
  p.free()
  return err
}/<code>

可以看到,fmt.Errorf裡面使用errors.New來創建error類型的實例,然後把格式化後字符串放入這個實例裡面。典型使用案例:

<code>const name, id = "bimmler", 17
err := fmt.Errorf("user %q (id %d) not found", name, id)  // user="bimmler" (id=17) not found
if err != nil {
  fmt.Print(err)
}/<code>

這樣,err不僅包含了格式化的錯誤消息,而且還是error類型,比我們先用fmt.Printf再用errors.New的寫法會更簡潔。

天殺錯誤判斷和處理邏輯

剛從其他語言轉到Go語言時,會非常不習慣Go語言的錯誤處理邏輯。在Java和Python裡面,我們可以使用throw來拋出異常,在另一個地方使用try...catch來捕獲異常。而Go語言鼓勵大家把錯誤放到返回值裡面,但是呢,又提供了panic和recover來拋出和捕獲異常。一般來說,我們把返回值裡面返回的叫做錯誤(因為他們都是error類型),把拋出和捕獲的叫做異常。

既然把錯誤放到返回值裡,那我們就每次都需要判斷返回裡的錯誤是否為空,才能知道是不是正常返回了。判斷和處理錯誤需要用到if,如果一段代碼裡面調用了很多函數,我們就能看到一連串的if,這種寫法感覺簡直就是瘋了!

<code>result1, err := err1()
if err != nil {
  fmt.Println(result1)
}
result2, err := err2()
if err != nil {
  fmt.Println(result2)
}
result3, err := err3()
if err != nil {
  fmt.Println(result3)
}
result4, err := err4()
if err != nil {
  fmt.Println(result4)
}/<code>

不過這個沒法避免,老老實實習慣一下吧!

panic/recover/defer

對於異常,Go語言可以使用panic來拋出一個恐慌性異常,一般來說,當程序遇到了比較大的問題,沒有辦法再執行下去,我們就用panic來拋出異常。來看一下panic源碼:

<code>func panic(v interface{})/<code>

panic接收一個interface{}類型的對象,我們上篇文章說過了,任何類型都是interface{}類型的子類型,因此,panic可以拋出任何對象。使用案例:

<code>func main() {
  panic("hello")
}/<code>

輸出:

<code>panic: hello

goroutine 1 [running]:
main.main()
    E:/goworkspace/blog/src/main/main.go:4 +0x40
/<code>

同時,我們可以使用recover來捕獲那些被panic拋出對象:

<code>func pa() string {
  panic("me")
}
func main() {
  p := pa()
fmt.Println(p)
if r := recover(); r != nil {
   fmt.Println(r)
  }
}/<code>

輸出:

<code>panic: me

goroutine 1 [running]:
main.pa(...)
    E:/goworkspace/blog/src/main/main.go:6
main.main()
    E:/goworkspace/blog/src/main/main.go:9 +0x40/<code>

怎麼好像recover沒起作用?這當然了,recover需要與defer一起使用,而且,recover的使用要放在panic拋出之前。下面的例子才對:

<code>func pa() string {
  panic("me")
}
func main() {

  defer func() {
    if r := recover(); r != nil {
      fmt.Println(r)  // 輸出:me
    }
  }()
  p := pa()  // 此行panic拋出me字符串
  fmt.Println(p)  // 此行不會執行
}/<code>

defer的用法

defer關鍵字表示,當前這個函數在退出之前,執行一下defer後面的邏輯。相當於我們委託一些操作給Go語言,在函數退出之前執行,有點像Java和Python裡面的finally作用了。

這樣,我們就可以把一些一定要執行的操作,放在defer中了,比如,我們關閉某個已經打開的資源:

<code>fsrc, err := os.Open("source.txt")
if err != nil {
  fmt.Println("open source file failed")
  return
}
defer fsrc.Close()  // 第一個defer
fdes, err := os.Open("target.txt")
if err != nil {
  fmt.Println("open target file failed")
  return
}
defer fdes.Close()  // 第二個defer
fmt.Println("do something here")/<code>

如果一個函數里面有多個defer,那麼在函數推出之前,會先執行最後一個defer,然後再執行倒數第二個、倒數第三個...,就像後進先出棧的操作。


我們下一篇聊聊通道和goroutine。


喜歡的點個關注!



分享到:


相關文章: