Go错误处理哲学
Go语言没有像C++、Java、Python等主流编程语言那样提供基于异常(exception)的结构化try-catch-finally错误处理机制,Go的设计者们认为将异常耦合到程序控制结构中会导致代码混乱,并且在那样的机制下,程序员会将大多常见错误(例如无法打开文件等)标记为异常,这与Go追求简单的价值观背道而驰。Go语言设计者们选择了C语言家族的经典错误机制:错误就是值,而错误处理就是基于值比较后的决策。同时,Go结合函数/方法的多返回值机制避免了像C语言那样在单一函数返回值中承载多重信息的问题。
Go中的错误不是异常,它就是普通值。Go这种简单的基于错误值比较的错误处理机制使得每个Go开发人员必须显式地关注和处理每个错误,经过显式错误处理的代码会更为健壮。
Go错误处理的四种策略
错误是值,只是以error接口变量的形式统一呈现(按惯例,函数或方法通常将error类型返回值放在返回值列表的末尾)。error接口是Go原生内置的类型,它的定义如下:
// $GOROOT/src/builtin/builtin.go
type interface error {
Error() string
}
任何实现了Error() string方法的类型的实例均可作为错误赋值给error接口变量。
透明错误处理策略
完全不关心返回错误值携带的具体上下文信息,只要发生错误就进入唯一的错误处理执行路径。
err := doSomething()
if err != nil {
// 不关心err变量底层错误值所携带的具体上下文信息
// 执行简单错误处理逻辑并返回
...
return err
}
透明错误处理策略最大限度地减少了错误处理方与错误值构造方之间的耦合关系,它们之间唯一的耦合就是error接口变量所规定的契约。
在这种策略下由于错误处理方并不关心错误值的上下文,因此错误值的构造方可以直接使用Go标准库提供的两个基本错误值构造方法errors.New和fmt.Errorf构造错误值。
err:=errors.New("xxxxx")
err:=fmt.Errorf("%d isn't an even number", i)
“哨兵”错误处理策略
上面的模式中如果错误处理方想做出错误处理路径的选取决策只能够尝试对返回的错误值进行检视。
if err != nil {
switch err.Error() {
case "error1":
//......
return
case "error2":
//....
return
}
}
这种反模式会造成严重的隐式耦合:错误值构造方不经意间的一次错误描述字符串的改动,都会造成错误处理方处理行为的变化,并且这种通过字符串比较的方式对错误值进行检视的性能也很差。
Go标准库采用了定义导出的(exported)“哨兵”错误值的方式来辅助错误处理方检视错误值并做出错误处理分支的决策:
// $GOROOT/src/bufio/bufio.go
var (
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
ErrBufferFull = errors.New("bufio: buffer full")
ErrNegativeCount = errors.New("bufio: negative count"
)
if err := doSomething(); err == bufio.ErrBufferFull {
// 处理缓冲区满的错误情况
...
}
与透明错误策略相比,“哨兵”策略让错误处理方在有检视错误值的需求时有的放矢。不过对于API的开发者而言,暴露“哨兵”错误值意味着这些错误值和包的公共函数/方法一起成为API的一部分。一旦发布出去,开发者就要对其进行很好的维护。而“哨兵”错误值也让使用这些值的错误处理方对其产生了依赖。
从Go 1.13版本开始,标准库errors包提供了Is方法用于错误处理方对错误值进行检视。Is方法类似于将一个error类型变量与“哨兵”错误值的比较:
if errors.Is(err, ErrOutOfBounds) {
// 越界的错误处理
}
如果使用的是Go 1.13及后续版本,请尽量使用errors.Is方法检视某个错误值是不是某个特定的“哨兵”错误值。
错误值类型检视策略
基于Go标准库提供的错误值构造方法构造的“哨兵”错误值并未提供其他有效的错误上下文信息。我们需要通过自定义错误类型的构造错误值的方式来提供更多的错误上下文信息,并且由于错误值均通过error接口变量统一呈现,要得到底层错误类型携带的错误上下文信息,错误处理方需要使用Go提供的类型断言机制(type assertion)或类型选择机制(type switch),这种错误处理称之为错误值类型检视策略。
// $GOROOT/src/encoding/json/decode.go
type UnmarshalTypeError struct {
Value string // description of JSON value - "bool", "array", "number -5"
Type reflect.Type // type of Go value it could not be assigned to
Offset int64 // error occurred after reading Offset bytes
Struct string // name of the struct type containing the field
Field string // the full path from root node to the field
}
// $GOROOT/src/encoding/json/decode_test.go
// 通过类型断言机制获取
func TestUnmarshalTypeError(t *testing.T) {
for _, item := range decodeTypeErrorTests {
err := Unmarshal([]byte(item.src), item.dest)
if _, ok := err.(*UnmarshalTypeError); !ok {
t.Errorf("expected type error for Unmarshal(%q, type %T): got %T",
item.src, item.dest, err)
}
}
}
// $GOROOT/src/encoding/json/decode.go
// 通过类型选择机制获取
func (d *decodeState) addErrorContext(err error) error {
if d.errorContext != nil && (d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0) {
switch err := err.(type) {
case *UnmarshalTypeError:
err.Struct = d.errorContext.Struct.Name()
err.Field = strings.Join(d.errorContext.FieldStack, ".")
}
}
return err
}
从Go 1.13版本开始,标准库errors包提供了As方法用于错误处理方对错误值进行检视。As方法类似于通过类型断言判断一个error类型变量是否为特定的自定义错误类型:
var e *MyError
if errors.As(err, &e) {
// 如果err类型为*MyError,变量e将被设置为对应的错误值
}
如果使用的是Go 1.13及后续版本,请尽量使用errors.As方法去检视某个错误值是不是某个自定义错误类型的实例。
错误行为特征检视策略
Go标准库中,我们发现了这样一种错误处理方式:将某个包中的错误类型归类,统一提取出一些公共的错误行为特征(behaviour),并将这些错误行为特征放入一个公开的接口类型中。以标准库中的net包为例,它将包内的所有错误类型的公共行为特征抽象并放入net.Error这个接口中。而错误处理方仅需依赖这个公共接口即可检视具体错误值的错误行为特征信息,并根据这些信息做出后续错误处理分支选择的决策。
// $GOROOT/src/net/net.go
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
// $GOROOT/src/net/http/server.go
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
参考资料
- 《Go语言精进之路》
- 《Go语言学习指南:惯例模式与编程实践》