Go error 简介

5,004 阅读4分钟

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的实现

  1. 创建了个errorString struct
  2. 实现Error方法
  3. 对外暴露一个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.EOFsyscall.ENOENTbufio.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
}