認識 Go 的 error 與學習判斷 error 類型的方法
Posted on Apr 16, 2024 in Go 程式設計 - 初階 by Amo Chen ‐ 6 min read
Go 的程式設計中,透過回傳 error 型別的值告知錯誤發生是相當常見的模式,所以很多函式的回傳定義大多如下所示,其中 1 個會是 error 型別,用以告知執行時的錯誤:
func FunctionName() (結果的型別, error)
這也是為什麼我們如果閱讀各種以 Go 開發的開放原始碼專案會很常看到類似以下的程式碼的原因:
r, err := FunctionName()
if err != nil {
// Error handling here
}
所以學會判斷 error 的類型並處理各種類型的錯誤,變成 Go 程式設計的重要課題。
本文將探討如何判斷 error 類型以及多種不同判斷的方法。
本文環境
- Go 1.12 以上
Go 的 error 是什麼?
探討如何判斷 error 之前,需要先認識 Go 的 error 到底是什麼。
Go 的 error 是 1 個 interface, 裡面只規定需要 1 個函式 Error() string
, error
的 interface 如下所示:
type error interface {
Error() string
}
所以無論型別為何,只要符合 error interface 的規定,有實作 Error() string
函式就可以視為是 error type, 譬如下列範例中的 FunctionName
回傳 A{"Unknown error"}
也沒問題,因為它有實作 Error() string
函式,符合 error interface 的規定,是合法的 error:
package main
import "fmt"
type A struct {
ErrStr string
}
func (a A) Error() string {
return a.ErrStr
}
func FunctionName() error {
return A{"Unknown error"}
}
func main() {
err := FunctionName()
fmt.Println(err.Error())
}
如何建立 Error?
建立 error 最簡單的方法是透過 errors
package 的 New()
函式,如下列範例 FunctionName() error
中回傳的 errors.New("Unknown error")
:
package main
import (
"errors"
"fmt"
)
func FunctionName() error {
return errors.New("Unknown error")
}
func main() {
err := FunctionName()
fmt.Println(err.Error())
}
這時就衍伸出我們最初的問題,如果有多種不同的 error 可能回傳,那要怎麼判別不同的 error, 例如下列函式可能會回傳 2 種不同的 error:
func Compute(v int) error {
if v == 0 {
return errors.New("v must > 0")
}
return errors.New("not implement yet")
}
有人可能會想說直接判斷 Error() string
回傳的字串:
func main() {
err := Compute(0)
if err != nil {
if err.Error() == "v must > 0" {
...
} else if err.Error() == "not implement yet" {
...
}
}
}
雖然這也是 1 種方法,但是這個方法有個大問題,例如 v must > 0
如果被改成 value must > 0
,我們必須修改所有 v must > 0
字串,確保所有的字串都相同才行,否則將會導致原有程式在判斷字串的部分失效。
也許比較好的辦法是用 err == <error type>
之類的判斷方式,所以在 Go 1.12 (含 1.12)之前,將 error.New()
抽出來變成共享的變數,再用 ==
進行判斷,是很常見的做法,
Go 1.12 (含 1.12)之前用 == 比較 error
Go 1.12 之前的範例:
package main
import (
"errors"
"fmt"
)
var ErrValueIsZero = errors.New("v is 0")
var ErrNotImplement = errors.New("not implement yet")
func Compute(v int) error {
if v == 0 {
return ErrValueIsZero
}
return ErrNotImplement
}
func main() {
err := Compute(0)
if err != nil {
if err == ErrValueIsZero {
fmt.Println(err.Error())
} else if err == ErrNotImplement {
fmt.Println(err.Error())
}
}
}
Go 1.13 之後改用 errors.Is() 比較 error
不過到了 Go 1.13 之後,用 ==
比較 error 的方式已經被棄用, Go 官方推薦使用 errors.Is()
方式進行比較,因此前述範例在 1.13 之後可以改成:
package main
import (
"errors"
"fmt"
)
var ErrValueIsZero = errors.New("v is 0")
var ErrNotImplement = errors.New("not implement yet")
func Compute(v int) error {
if v == 0 {
return ErrValueIsZero
}
return ErrNotImplement
}
func main() {
err := Compute(0)
if err != nil {
if errors.Is(err, ErrValueIsZero) {
fmt.Println(err.Error())
} else if errors.Is(err, ErrNotImplement) {
fmt.Println(err.Error())
}
}
}
更複雜的 error 設計如何比較
在 error 訊息中添加更詳細的資訊,幫助開發者更容易理解錯誤,也是很常見的需求,譬如找不到頁面的錯誤,我們想增加 URL 的資訊在錯誤訊息之中,可能會這樣寫:
func LoadPage(p string) error {
if p == "/" return nil
return errors.New("page %s not found", p)
}
這種寫法導致我們無法用 ==
單純進行比較,因為我們不知道 p
切確的值是什麼,所以我們需要更複雜一些的 error 設計,例如將 p
變成 1 個結構中的欄位,並且為該結構實作 Error() string
函式:
type ErrPageNotFound struct {
Path string
}
func (e ErrPageNotFound) Error() string {
return fmt.Sprintf("page %s not found", e.Path)
}
用 type assertion 判斷 error
To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.
針對前述更複雜一些的 error 設計,我們可以用 type assertion 進行,例如以下範例中的 _, ok := err.(ErrPageNotFound)
就是 type assertion 的用法,可以幫助我們判斷 error 是否屬於特定的 error type, 如此一來不管 Path
怎麼變,我們都還是能正確判斷該錯誤:
package main
import (
"fmt"
)
type ErrPageNotFound struct {
Path string
}
func (e ErrPageNotFound) Error() string {
return fmt.Sprintf("page %s not found", e.Path)
}
func LoadPage(p string) error {
if p == "/" {
return nil
}
return ErrPageNotFound{p}
}
func main() {
err := LoadPage("/p/1")
_, ok := err.(ErrPageNotFound)
if ok {
fmt.Println("err is ErrPageNotFound")
}
}
其執行結果為:
err is ErrPageNotFound
Go 1.13 之後可以使用 errors.As() 判斷 error type
Go 1.13 (含 1.13)之後支援用 errors.As()
判斷 error type:
func As(err error, target any) bool
errors.As()
的第 2 個參數必須是 non-nil 的指標(pointer),該指標必須指向 1 個有實作 error interface 的 type 。
因此前述用 type assertion 進行判斷的方式,可以改成:
package main
import (
"errors"
"fmt"
)
type ErrPageNotFound struct {
Path string
}
func (e ErrPageNotFound) Error() string {
return fmt.Sprintf("page %s not found", e.Path)
}
func LoadPage(p string) error {
if p == "/" {
return nil
}
return ErrPageNotFound{p}
}
func main() {
err := LoadPage("/p/1")
if errors.As(err, &ErrPageNotFound{}) {
fmt.Println("err is ErrPageNotFound")
}
}
上述範例其實只是將 type assertion 的部分改為 errors.As(err, &ErrPageNotFound{})
而已,其執行結果為:
err is ErrPageNotFound
談談何謂 Wrapped errors 與其用途
Go 1.13 之後支援 wrapped errors, 也就是說可以對 error 進行再包裝:
An error e wraps another error if e’s type has one of the methods:
- Unwrap() error
- Unwrap() []error
只要 error 實作 Unwrap() error
或 Unwrap() []error
其中 1 個方法,就屬於 wrapped errors 。
error 再包裝的最簡單做法是使用 fmt.Errorf 並搭配 %w
verb, 例如:
ErrUnknown := fmt.Errorf("unknown error: %w", err)
大家可能會疑問為什麼要這樣做?這樣做有什麼好處?
我們可以看 1 個範例:
package main
import (
"errors"
"fmt"
)
type ErrInvalidInput struct {
Value interface{}
}
func (e ErrInvalidInput) Error() string {
return fmt.Sprintf("invalid input %v", e.Value)
}
var ErrDeprecatedFunc = errors.New("This func is deprecated. Don't use it.")
func Compute(p int) error {
if p == 0 {
return ErrInvalidInput{p}
}
return fmt.Errorf("func: Compute | %w", ErrDeprecatedFunc)
}
func CallCompute(p int) error {
err := Compute(1)
if err != nil {
if errors.As(err, &ErrInvalidInput{}) {
fmt.Println("error handling here")
} else {
return fmt.Errorf("unknown error | %w", err)
}
}
return nil
}
func main() {
err := CallCompute(1)
fmt.Println(err.Error())
}
範例執行結果如下,可以看到我們對 error 再包裝,增加了更多有用的除錯資訊:
unknown error | func: Compute | This func is deprecated. Don't use it.
上述範例中的 Compute(p int) error
函式利用 wrapped errors 的功能,將共享的 ErrDeprecatedFunc
再包裝,把自身的函數名稱也加到 error 訊息之中,方便他人除錯,如此也不用特別設計 1 種錯誤。
另外 CallCompute(p int)
函式只關注 ErrInvalidInput
的錯誤,其他類型的錯誤它就使用 fmt.Errorf("[Compute] unknown error: %w", err)
對 error 再包裝後直接回傳,透過再包裝的方法,同樣可以不用特別設計 1 種錯誤。
那麽要怎麼判斷這種再包裝後的 error 呢?其實 Go 1.13 之後的 errors.Is()
與 errors.As()
支援遞迴形式對 error 進行 unwrap, 所以只要 unwrap 的過程 errors.Is()
或 erros.As()
結果為 true
就代表 2 者錯誤相同,所以前述範例的 err := CallCompute(1)
可以用 errors.Is()
進行判斷,不用管它被包裝過幾次(本文中被包裝過 2 次):
func main() {
err := CallCompute(1)
if err != nil {
if errors.Is(err, ErrDeprecatedFunc) {
fmt.Println("err is ErrDeprecatedFunc")
} else if errors.As(err, &ErrInvalidInput{}) {
fmt.Println("err is ErrInvalidInput")
}
}
}
它的好處在於我們可以為 error 進行包裝,除了利用既有的 error 之外,也能加入更多錯誤訊息、功能。
實作 Unwrap() error 實現階層式(hierarchy)的 error
Python 的 Exception 其實有定義階層,例如:
BaseException
├── BaseExceptionGroup
├── GeneratorExit
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
這種階層式的作法,可以讓開發者捕捉較高層級的 exception 就好,不用一項一項 exception 都要處理,例如開發者可以直接捕捉 ValueError
就好,不用管它底下的 UnicodeError
或者 UnicodeDecodeError
,相同的設計也出現在各種 Python 套件之中。
Go 則可以實作 Unwrap()
做到類似的事情,例如我們想實作 3 階層的 error 的話:
ErrBase
└── ErrValue
└── ErrInvalidInput
可以這樣設計:
package main
import (
"errors"
"fmt"
)
var ErrBase = errors.New("base error")
type ErrValue struct{}
func (e ErrValue) Error() string {
return fmt.Sprint("ErrValue")
}
func (e ErrValue) Unwrap() error {
return ErrBase
}
type ErrInvalidInput struct{}
func (e ErrInvalidInput) Error() string {
return fmt.Sprint("ErrInvalidValue")
}
func (e ErrInvalidInput) Unwrap() error {
return ErrValue{}
}
func main() {
err := ErrInvalidInput{}
fmt.Printf("%t\n", errors.As(err, &ErrInvalidInput{}))
fmt.Printf("%t\n", errors.As(err, &ErrValue{}))
fmt.Printf("%t\n", errors.Is(err, ErrBase))
}
上述範例的 ErrInvalidInput
執行 Unwrap()
之後,回傳的是 ErrValue
, 而進一步執行 ErrValue
的 Unwrap()
之後,回傳的是 ErrBase
, 如下圖所示:
搭配 errors.As()
與 errors.Is()
遞迴 unwrap 的特性,所以在 main()
函式中的 3 個比較結果都為 true
,這就是如何在 Go 做到類似 Python 階層式 exception 的方式。
總結
Go 的 error handling 是相當重要的課題。
儘管 Go 不像 Python, Java, JavaScript 等程式語言,並沒有 try-catch 可以使用,但其設計哲學是為了讓開發者能夠思考各種可能出現的問題,並針對這些問題提供適當的處理,而非僅僅只是捕捉錯誤,然後只做一般的處理。
所以對於習慣 Python, JavaScript 等語言的開發者來說,使用 Go 處理 error 不免會感到疑惑或者煩悶,但只要詳細了解它的設計與哲學,多半會釋懷一些。
以上!
Enjoy!
References
errors package - errors - Go Packages
Error handling and Go - The Go Programming Language