Go error
我们经常会看到Go的运行代码中充斥着这样的判断if err != nil
。与Java或C#等其他语言处理异常不同,显得代码有些冗余。让我们更深入的了解一下Go error的设计。
error interface
我们来看一下error对象,错误内置接口类型,是代表错误条件的常规接口,nil值代表没有错误。
// 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
}
go error的实现
- 创建了个errorString struct
- 实现Error方法
- 对外暴露一个Function, New方法来创建,需要重点关注
return &errorString{text}
,返回的是指针对象,而非对象的值。
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
}
这里为什么是返回指针呢?
举个例子,我们有A、B两个模块都定义一个异常errors.New("业务错误")
。如果errors.New返回的是值,做等值比较时他会展开errorString
里面的s
做值对比,就会导致这两个errors相等。
Error vs Exception
Java C# Exception
在Java和C#中,我们通过catch(exception e){// ignore }
来进行异常捕获,从而各种方法会抛出各种Exception,从而使异常变得司空见惯,异常变得不那么严重了,本来是一个友好的机制,结果变成了开发人员绕开的捷径。
Go error
go语言天生支持多返回值,类似于其他语言的元组。如果一个函数返回了error,你就必须优先判断error,这也是就是我们为什么会看到那么多的if err != nil
。
- 使用简单
- 优先考虑失败,而不是成功。
- 没有隐含的控制流
- 完全交给你来控制error
panic&recover
- Go panic 意味着fatal error。意味着代码不能继续运行。
- Go recover 保证服务不会挂掉
func panicAndRecover() {
defer func() {
errR := recover()
fmt.Println(errR)
}()
panic("demo panic")
}
panic recover看似类似与try catch,但是go 语言中是不建议这么玩的。对于真正以外的情况,那些标识不可回复的程序错误,如索引越界、不可恢复的环境问题、栈溢出,我们才使用panic。对于其他的错误情况,我们期望使用error进行判定。
error 使用
Sentinel Error
预定义的特定错误,如:io.EOF
、syscall.ENOENT
、bufio.ErrInvalidUnreadByte
。
// EOF is the error returned by Read when no more input is available.
// (Read must return EOF itself, not an error wrapping EOF,
// because callers will test for EOF using ==.)
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF")
package bufio
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")
)
Error Types
PathError
Error Types的方式提供了更为全面的上下文,可以协助开发人员更精确的定位问题。
os.PathError源码为例:它提供了底层执行了什么操作,具体的路径信息;
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
func (e *PathError) Unwrap() error { return e.Err }
// Timeout reports whether this error represents a timeout.
func (e *PathError) Timeout() bool {
t, ok := e.Err.(interface{ Timeout() bool })
return ok && t.Timeout()
}
Opaque errors
不透明的错误处理。使用者无需关心内部,只需要关心成功还是失败。只需返回错误结果而不假设其内容。如下:
func fileOp() error {
_, err := os.Open("/path")
if err != nil {
return err
}
// read or write
return nil
}
在少数情况下,上面这种异常处理不够友好。我们可以进一步加强Opaque errors。
- Assert errors for behaviour, not type. 对外暴露行为,非errors.
package net
// An Error represents a network error.
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
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()
}
func TestDeadlineExceededIsNetError(t *testing.T) {
err, ok := context.DeadlineExceeded.(net.Error)
if !ok {
t.Fatal("DeadlineExceeded does not implement net.Error")
}
if !err.Timeout() || !err.Temporary() {
t.Fatalf("Timeout() = %v, Temporary() = %v, want true, true", err.Timeout(), err.Temporary())
}
}
handle error
真正的项目中我们如何处理error呢? go 的error 是一个值类型,不像java的exception会具有堆栈信息,当我们执行一段业务代码,自底向上返回了一个错误信息如:连接超时,假如业务十分复杂,没有具体的堆栈信息,我们难以排查问题。由此我们引入了warp error
warp error
package github.com/pkg/errors
- 借助errors包,通过wrap的方式帮助我们添加上下文,以及堆栈信息。
- 使用 errors.Cause 获取 root error,再和 sentinel error 判定。
func main() {
err := WriteConfig(os.Stdout)
if err != nil {
fmt.Printf("original error : %T %v\n", errors.Cause(err), errors.Cause(err))
fmt.Printf("stack trace : \n %+v\n", err)
}
}
func WriteConfig(w io.Writer) error {
_, err := os.Open("")
if err != nil {
err = errors.Wrapf(err, "open fail")
}
return err
}