天天看點

Go語言學習 二十三 錯誤處理和運作時恐慌(Panic)一 錯誤二 Panic(恐慌)三 Recover(恢複)

本文最初發表在我的個人部落格,檢視原文,獲得更好的閱讀體驗

一 錯誤

1.1 error類型

按照約定,Go中的錯誤類型為

error

,這是一個内建接口,

nil

值表示沒有錯誤:

type error interface {
        Error() string
}
           

我們可以很友善的自定義一個錯誤類型:

package main

import (
	"fmt"
)

func main() {
	e := MyError{"This is a custom Error Type."}
	fmt.Println(e.Error())

	v1, err := divide(10, 2)
	if err == nil {
		fmt.Println(v1)
	}

	if v2, err := divide(5, 0); err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(v2)
	}
}

// 自定義錯誤類型
type MyError struct {
	msg string
}

func (e *MyError) Error() string {
	return e.msg
}

// 取整除法
func divide(a1, a2 int) (int, error) {
	if a2 == 0 {
		return 0, &MyError{"整數相除,除數不能為零"}
	}

	return a1 / a2, nil
}
           

上述

divide

函數會傳回一個

error

值,調用方可以根據這個錯誤值來判斷如何處理結果。這種用法在Go中是一種慣用法,尤其在編寫一些函數庫之類的功能時。例如标準庫

os

中的打開檔案的

Open

函數定義如下:

// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
	return OpenFile(name, O_RDONLY, 0)
}
           

該函數傳回的具體錯誤類型為

PathError

// PathError records an error and the operation and file path that caused it.
type PathError struct {
	Op   string
	Path string
	Err  error
}

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

該錯誤詳細的描述了引發錯誤的操作以及相關檔案路徑和錯誤描述資訊。

1.2 其他錯誤類型

除此之外,标準庫中還有許多其他預定義的錯誤類型,它們都直接或間接的實作(或内嵌)了

error

接口。例如:

runtime.Error       // 表示運作時錯誤的Error接口類型
net.Error           // 表示網絡錯誤的Error接口類型
go/types.Error      // 表示類型檢查錯誤的Error結構類型
html/template.Error // 表示html模闆轉義期間遇到的問題(結構類型)
os/exec             // 當檔案不是一個可執行檔案時傳回的錯誤(結構類型)
           

另外,标準庫中的

errors

包提供了一個函數可友善地傳回一個

error

執行個體:

// New 函數傳回格式為給定文本的錯誤
func New(text string) error
           

它的具體實作如下:

// errors 包實作了操作錯誤的函數
package errors

// New 函數傳回格式為給定文本的錯誤
func New(text string) error {
	return &errorString{text}
}

// errorString 是 error 的一個簡單實作(注意是私有的)
type errorString struct {
	s string
}

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

示例:

package main

import (
	"errors"
	"fmt"
)

func main() {
	fmt.Println(errors.New("這是一條錯誤資訊"))	
}
           

如果上述錯誤描述過于簡單,還可以使用

fmt

包中的

Errorf

函數:

// Errorf根據指定的格式進行格式化參數,并傳回滿足error接口的字元串
func Errorf(format string, a ...interface{}) error {
	return errors.New(Sprintf(format, a...))
}
           

該函數允許我們使用軟體包的格式化功能來建立描述性錯誤消息:

package main

import (
	"fmt"
)

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

通常,以上兩種方法能滿足絕大多數錯誤場景。如果仍然不夠,正如本文開頭所講,你可以自定義任意的錯誤類型。

二 Panic(恐慌)

内建函數

panic

可以産生一個運作時錯誤,一旦調用該函數,目前

goroutine

就會停止正常的執行流程。這種情況一般發生在一些重要參數缺失的檢查時,因為如果缺失了這些參數,将導緻程式不能正常運作,故相比讓程式繼續運作(也可能根本就沒法正常運作),不如讓它及時終止。

該函數接受一個任意類型的實參(一般為字元串),并在程式終止時列印。

package main

func main() {
	panic("運作出錯了。")
}
           

另一類使用場景:

package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("wait for init...")
}

var user = os.Getenv("USER")

func init() {
    // 檢查必要變量等
	if user == "" {
		panic("no value for $USER")
	}
}
           
一般情況下,我們應避免使用panic,尤其是在庫函數中。

panic

被調用後(包括不明确的運作時錯誤,例如數組或切片索引越、類型斷言失敗)等,程式将立刻終止目前函數的執行,并開始回溯

goroutine

的棧,運作任何被推遲的函數。若回溯到達

goroutine

棧的頂端,程式就會終止。

假設函數

F

調用了

panic

,則

F

的正常執行将立即停止。

F

中任何被推遲的函數将依次執行,然後

F

傳回到調用處。對于調用者

G

,此時好像也在調用

panic

函數一樣,執行到此立即停止,并開始回溯所有被

G

推遲的函數。就這樣一直回溯,直到該

goroutine

中的所有函數都停止,此時,程式終止,并報告錯誤資訊,包括傳給

panic

的參數。

當然,我們還可以使用内建函數

recover

進行恢複,奪回

goroutine

的控制權,繼續往下看。

我們在defer語句一文中提到過,

defer

棧是以

LIFO

的順序執行的。

三 Recover(恢複)

内建函數

recover

可以讓發生

panicking

goroutine

恢複正常運作。在一個被推遲的函數中執行

recover

可以終止

panic

的産生的終止回溯調用。注意必須是直接在被推遲的函數中。如果不是在推遲函數中(或間接)調用該函數,則不會發生任何作用,将傳回

nil

。如果程式沒有發生

panic

panic

的參數為

nil

,則

recover

的傳回值也為

nil

以下示例展示了panic和recover的工作機制:

package main

import "fmt"

func main() {
	f()
	fmt.Println("從 f() 中正常傳回。")
}

func f() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("從 f() 中正常恢複。", r)
		}

	}()
	fmt.Println("開始調用函數 g()。。。")
	g(0)
	fmt.Println("從 g() 中正常傳回。")
}

func g(i int) {
	if i > 3 {
		fmt.Println("Panicking!")
		panic(fmt.Sprintf("%v", i))
	}
	defer fmt.Println("函數g()中推遲的調用", i)
	fmt.Println("函數g()中的列印", i)
	g(i + 1)
}

func h() {
	fmt.Println("hello")
}
           

看一個effective_go中的例子:

在伺服器中終止失敗的

goroutine

而無需殺死其它正在執行的

goroutine

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}
           

通過恰當地使用恢複模式,

do

函數(及其調用的任何代碼)可通過調用

panic

來避免更壞的結果。我們可以利用這種思想來簡化複雜軟體中的錯誤處理。

再看一個

regexp

包的理想化版本,它會以局部的錯誤類型調用

panic

來報告解析錯誤。以下是一個

Error

類型,一個

error

方法和一個

Compile

函數的定義:

//  Error 是解析錯誤的類型,它滿足 error 接口。
type Error string
func (e Error) Error() string {
    return string(e)
}

// error 是 *Regexp 的方法,它通過用一個 Error 觸發Panic來報告解析錯誤。
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile 傳回該正規表達式解析後的表示。
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // 如果有解析錯誤,doParse會産生panic
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // 清理傳回值
            err = e.(Error) // 若它不是解析錯誤,将重新觸發Panic。
        }
    }()
    return regexp.doParse(str), nil
}
           

如果

doParse

觸發了

panic

,恢複塊會将傳回值設為

nil

—被推遲的函數能夠修改已命名的傳回值。在

err

的指派過程中,我們将通過斷言它是否擁有局部類型

Error

來檢查它。若它沒有,類型斷言将會失敗,此時會産生運作時錯誤,并繼續棧的回溯,仿佛一切從未中斷過一樣。該檢查意味着若發生了一些像索引越界之類的意外,那麼即便我們使用了

panic

recover

來處了解析錯誤,代碼仍然會失敗。

通過适當的錯誤處理,

error

方法(由于它是個綁定到具體類型的方法,是以即便它與内建的

error

類型名字相同也沒有關系)能讓報告解析錯誤變得更容易,而無需擔心手動處理回溯的解析棧:

if pos == 0 {
    re.error("'*' illegal at start of expression")
}
           

盡管這種模式很有用,但它應當僅在包内使用。

Parse

會将其内部的

panic

調用轉為

error

值,它并不會向調用者暴露出

panic

。這是個值得遵守的良好規則。

另外,這種重新觸發

panic

的慣用法會在産生實際錯誤時改變

panic

的值。然而,不管是原始的還是新的錯誤都會在崩潰報告中顯示,是以問題的根源仍然是可見的。這種簡單的重新觸發

panic

的模型已經夠用了,畢竟它隻是一次崩潰。但若你隻想顯示原始的值,也可以多寫一點代碼來過濾掉不需要的問題,然後用原始值再次觸發

panic

參考:

https://golang.org/doc/effective_go.html#errors

https://golang.org/pkg/builtin/#error

https://blog.golang.org/defer-panic-and-recover