本文最初發表在我的個人部落格,檢視原文,獲得更好的閱讀體驗
一 錯誤
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