標準錯誤
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。
喜歡的點個關注!