Go语言精进之路读书笔记第37条——了解错误处理的4种策略
C语言家族的经典错误机制:错误就是值。同时Go结合函数/方法的多返回值机制避免了像C语言那样在单一函数返回值种承载多重信息的问题。
37.1 构造错误值
错误处理的策略与构造错误值的方法是密切关联的。
错误是值,只是以error接口变量的形式统一呈现(按惯例,函数或方法通常将error类型返回值放在返回值列表的末尾)。
error接口是Go原生内置的类型,它的定义如下:
// $GOROOT/src/builtin/builtin.go
// 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
}
任何实现了Error() string方法的类型的实例均可作为错误赋值给error接口变量。
在标准库中,Go提供了构造错误值的两种基本方法——errors.New
和fmt.Errorf
,示例如下:
err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("inedx %d is out of bounds", i)
wrapErr = fmt.Errorf("wrap error: %w", err) // 仅Go 1.13及后续版本可用
Go 1.13版本之前,这两种方法实际上返回的是同一个实现了error接口的类型的实例,这个未导出的类型就是errors.errorString
:
// $GOROOT/src/errors/errors.go
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
Go 1.13及后续版本中,当我们在格式化字符串中使用%w
时,fmt.Errorf
返回的错误值的底层类型为fmt.wrapError
:
// $GOROOT/src/fmt/errors.go (Go 1.13及后续版本)
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
与errorString相比,wrapError多实现了Unwrap方法,这使得被wrapError类型包装的错误值在包装错误链中被检视(inspect)到:
var ErrFoo = errors.New("the underlying error")
err := fmt.Errorf("wrap err: %w", ErrFoo)
errors.Is(err, ErrFoo) // true (仅Go 1.13及后续版本可用)
标准库中的net包定义了一种携带额外错误上下文的错误类型
// $GOROOT/src/net/net.go
type OpError struct {
Op string
Net string
Source string
Addr Addr
Err Error
}
扩展:判断是否实现了接口
// $GOROOT/src/errors/wrap.go:39
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// TODO: consider supporting target.Is(err). This would allow
// user-definable predicates, but also may allow for coping with sloppy
// APIs, thereby making it easier to get away with them.
if err = Unwrap(err); err == nil {
return false
}
}
}
37.2 透明错误处理策略
完成不关心返回错误值携带的具体上下文信息,只要发生错误就进入唯一的错误处理执行路径。
err := doSomething()
if err != nil {
// 不关心err变量底层错误值所携带的具体上下文信息
// 执行简单错误处理逻辑并返回
...
return err
}
37.3 “哨兵”错误处理策略
// $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")
)
// 错误处理代码
data, err := b.Peek(1)
switch err {
case bufio.ErrNegativeCount:
// ...
return
case bufio.ErrBufferFull:
// ...
return
case bufio.ErrInvalidUnreadByte:
// ...
return
default:
// ...
return
}
}
// 或者
if err := doSomething(); err == bufio.ErrBufferFull {
// 处理缓冲区满的错误情况
...
}
一般哨兵错误值变量以ErrXXX格式命名。暴露“哨兵”错误值意味着这些错误值和包的公共函数/方法一起成为API的一部分。
从Go 1.13版本开始,标准库errors提供了Is方法用于错误处理方对错误值进行检视。Is方法类似于将一个error类型变量与“哨兵”错误值的比较:
// 类似 if err == ErrOutOfBounds{ ... }
if errors.Is(err, ErrOutOfBounds) {
越界的错误处理
}
不同的是,Is方法会沿着错误链与链上所有被包装的错误进行比较,直到找到一个匹配的错误。
37.4 错误值类型检视策略
通过自定义错误类型的构造错误值的方式来提供更多的错误上下文信息,并且由于错误值均通过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
// addErrorContext returns a new error enhanced with information from d.errorContext
func (d *decodeState) addErrorContext(err error) error {
if 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
}
}
return err
}
一般自定义导出的错误类型以XXXErr格式命名。与“哨兵”错误处理策略一样,这些错误类型和包的公共函数/方法一起成为API的一部分。
As方法可以检视某个错误值是不是某个自定义错误类型的实例。
As方法和Is方法一样,会沿着错误链与链上所有被包装的错误进行比较,直到找到一个匹配的错误。
37.5 错误行为特征检视策略
将某个包中的错误类型归类,统一提取出一些公共的错误行为特征(behavior),并将这些错误行为特征放入一个公开的接口类型中。
// $GOROOT/src/net/net.go
// An Error represents a network error.
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
// $GOROOT/src/net/http/server.go
func (srv *Server) Serve(l net.Listener) error {
...
for {
rw, err := l.Accept()
if err != nil {
select {
case <-srv.getDoneChan():
return ErrServerClosed
default:
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
// 这里对临时性错误进行处理
...
time.Sleep(tempDelay)
continue
}
return err
}
...
}
...
}
// $GOROOT/src/net/net.go
type OpError struct {
...
// Err is the error that occurred during the operation.
// The Error method panics if the error is nil.
Err error
}
type temporary interface {
Temporary() bool
}
func (e *OpError) Temporary() bool {
// Treat ECONNRESET and ECONNABORTED as temporary errors when
// they come from calling accept. See issue 6163.
if e.Op == "accept" && isConnError(e.Err) {
return true
}
if ne, ok := e.Err.(*os.SyscallError); ok {
t, ok := ne.Err.(temporary)
return ok && t.Temporary()
}
t, ok := e.Err.(temporary)
return ok && t.Temporary()
}