C vs Go
Go 语言的错误处理机制是在 C 语言错误处理机制基础上的再创新
通常使用类型为整型
的函数返回值作为错误状态标识
,函数调用者会基于值比较
的方式来处理错误
返回值为 0,代表函数调用成功,否则函数调用出现错误
优点
要求开发者必须显式
地关注
和处理
每个错误
错误一般为普通值
,不需要额外的语言机制
来处理,让代码更容易调试
C 语言错误处理机制具有简单
和显式
结合的特征,非常符合 Go 的设计哲学
缺点
C 语言中的函数最多仅支持一个返回值
一值多用
承载函数要返回给调用者的信息
承载函数调用的最终错误状态
当返回值为其它类型,如字符串,很难将数据
与错误
融合在一起
因此 Go 函数新增了多返回值机制
,用于支持数据
与错误
(返回值列表的末尾
)的分离
fmt/print.go 1 2 3 4 5 6 7 8 9 func Fprintf (w io.Writer, format string , a ...interface {}) (n int , err error ) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return }
builtin/builtin.go 1 2 3 4 5 type error interface { Error() string }
error 常用
只能提供字符串
形式的错误上下文
1 2 e1 := errors.New("this is an error" ) e2 := fmt.Errorf("index %d is out of bounds" , 10 )
errors/errors.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func New (text string ) error { return &errorString{text} } type errorString struct { s string } func (e *errorString) Error() string { return e.s }
自定义 net/net.go 1 2 3 4 5 6 7 type OpError struct { Op string Net string Source Addr Addr Addr Err error }
利用类型断言
(Type Assertion,判断接口类型
的动态类型
),判断 err 的动态类型
net/http/server.go 1 2 3 4 5 6 7 8 9 10 11 12 func isCommonNetReadError (err error ) bool { if err == io.EOF { return true } if neterr, ok := err.(net.Error); ok && neterr.Timeout() { return true } if oe, ok := err.(*net.OpError); ok && oe.Op == "read" { return true } return false }
优点
统一
了错误类型
错误是值
易扩展
,支持自定义
的错误上下文
error
接口是契约
,具体的错误上下文与 error 接口解耦
,体现 Go 组合
设计哲学中的正交
理念
策略
透明
> 行为
> 哨兵/类型
(耦合)
透明
完全不关心
返回错误值携带的具体上下文信息 - 最常见
(> 80%)+ 解耦
1 2 3 4 5 6 7 8 9 10 11 func foo () error { return errors.New("some error occurred" ) } func bar () error { err := foo() if err != nil { return err } return nil }
哨兵
反模式
:错误处理方以透明错误值
所能提供的唯一
上下文信息(字符串
),作为错误处理路径选择的依据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func foo () error { return errors.New("some error occurred" ) } func bar () error { err := foo() if err != nil { switch err.Error() { case "some error occurred" : return errors.New("some other error occurred" ) case "some other error occurred" : return errors.New("some other other error occurred" ) default : return err } } return nil }
造成严重的隐式耦合
,可以通过导出哨兵错误值
的方式来辅助
错误处理方检视错误值并做出错误处理分支的决策
bufio/bufio.go 1 2 3 4 5 6 7 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" ) )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "bufio" ) func main () { reader := bufio.Reader{} _, err := reader.Peek(1 ) if err != nil { switch err { case bufio.ErrBufferFull: return case bufio.ErrNegativeCount: return default : return } } }
从 Go 1.13
开始,errors.Is
函数用于错误处理方对错误值的检视
如果 error 类型变量的底层错误值
是一个包装错误
(Wrapped Error)
errors.Is
函数会沿着该包装错误
所在的错误链
(Error Chain)
与链上所有被包装的错误进行比较,直到找到一个匹配的错误为止
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var ErrUnderlying = errors.New("underlying error" )func main () { err1 := fmt.Errorf("wrap underlying: %w" , ErrUnderlying) err2 := fmt.Errorf("wrap err1: %w" , err1) println (err1 == ErrUnderlying) println (err2 == err1) println (err2 == ErrUnderlying) println (errors.Is(err1, ErrUnderlying)) println (errors.Is(err2, err1)) println (errors.Is(err2, ErrUnderlying)) }
类型
通过自定义错误类型和构造错误值的方式,来提供更多的错误上下文
错误值都是通过 error
接口变量统一
呈现 因此要依赖 Go 的类型断言
机制(Type Assertion
)和类型选择
机制(Type Switch
)
encoding/json/decode.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type UnmarshalTypeError struct { Value string Type reflect.Type Offset int64 Struct string Field string } 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 }
从 Go 1.13
开始,errors.As
函数用于错误处理方对错误值的检视
errors.As
函数类似于通过类型断言
判断一个 error 类型变量是否为特定的自定义错误类型
如果 error 类型变量的动态错误值
是一个包装错误
,errors.As
函数会沿着该包装错误所在的错误链
与链上所有被包装的错误的类型进行比较,直到找到一个匹配的错误类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 type MyErr struct { e string } func (m *MyErr) Error() string { return m.e } func main () { var myErr = &MyErr{"my myErr" } err1 := fmt.Errorf("wrap myErr: %w" , myErr) err2 := fmt.Errorf("wrap err1: %w" , err1) var e *MyErr if errors.As(err1, &e) { println (e == myErr) fmt.Printf("%p, %T\n" , myErr, myErr) fmt.Printf("%p, %T\n" , e, e) } if errors.As(err2, &e) { println (e == myErr) fmt.Printf("%p, %T\n" , myErr, myErr) fmt.Printf("%p, %T\n" , e, e) } }
行为
如何降低错误处理方
和错误构造方
的耦合?(解耦
- 透明;耦合
- 哨兵、类型)
将某个包中错误类型归类
,统一提取公共
的错误行为特征
,并将这些错误行为特征放在一个公开
的接口类型
中
net/net.go 1 2 3 4 5 6 type Error interface { error Timeout() bool Temporary() bool }
错误处理方只需要依赖该公共接口
net/http/server.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, err := l.Accept() if err != nil { select { case <-srv.getDoneChan(): return ErrServerClosed default : } 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 } return err } connCtx := ctx if cc := srv.ConnContext; cc != nil { connCtx = cc(connCtx, rw) if connCtx == nil { panic ("ConnContext returned nil" ) } } tempDelay = 0 c := srv.newConn(rw) c.setState(c.rwc, StateNew, runHooks) go c.serve(connCtx) }
Accept 实际返回的错误类型为 *OpError
net/net.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type OpError struct { Op string Net string Source Addr Addr Addr Err error } type temporary interface { Temporary() bool } func (e *OpError) Temporary() bool { 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() }