GO— 异常错误处理

error标识符

概述

error是go预定义的标识符。任何实现error接口的都可以认为是一个error,此接口定义如下:

type error interface {
    Error() string
}

所以,任何实现Error() string方法的类型结构都可以作为error使用。 go标准库的errors包提供了创建error类型的函数New(),可以这样使用

func c(i int) error{
	if i<0 {
		return errors.New("i is a negative number")
	}
	return nil
}

我们看下new的实现:

package errors
func New(text string) error {
	return &errorString{text}
}
type errorString struct {
	s string
}
func (e *errorString) Error() string {
	return e.s
}

new是text参数作为errors包下私有类似errorString的初始化参数,Error()的指针接收者为errorString,也就是说,errorString类型实现了erorr的Error() string方法,他就是一个error类型。new就是返回一个error类型值。

可以产生error类型值的另一个内建方式:

fmt.Errorf("%d is a negative number",i)

他也是通过errors.new 和Sprintf实现,并不会在屏幕打印字符串。

自定义

有时候,调用一个方法,我们需要根据返回error类型做不同的操作,比如http 4xx和5xx错误打印不同的日志等。可以定义自己的错误类型;自定义其实就跟new函数实现类似。

var e4xxError = errors.New("4xx")
func main() {
	if err:=c(400);err == e4xxError{
		fmt.Println(err)
		return
	}
	fmt.Println("unknown err")
}
func c(i int) error{
	if i<500 && i>=400 {
		return e4xxError
	}
	return errors.New("no 4xx error")
}

但是使用==比较,只能是完全相同的变量值。这里通常指作为一个错误信号使用。

对于需要复杂的错误对象,比如error codes等,这种方式不太好,还是应该创建自己error实现类型结构,需要注意的是,返回值推荐设置为error类型,而非自定义错误类型。通常为了扩展复杂的错误信息,可以如下实现:

type Error interface {
    error
    Timeout() bool  
    Temporary() bool 
}

调用时,通过类型断言err是不是net.Error,来细化错误的处理,比如使用switch语句判断。

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
	time.Sleep(1e9)
	continue
}
if err != nil {
	log.Fatal(err)
}

压缩代码行数

d, err := F()
if err != nil {
	fmt.Println(err)
	return err
}

c, err := F1()
if err != nil {
	fmt.Println(err)
	return err
}

编程go时,经常有这种代码,每调用一次函数判断一次,可以换一种方式编写:

var d int
if d, err := F(); err != nil {
	fmt.Println(err) // some int
	return err
}
fmt.Println(d) // 0

统一处理

有时候,业务代码不需要没走不都判断错误,可以有一个统一错误处理方法解决;

  1. 把错误都定义在同一个包下;
// Kubernetes 源码
package errors
type StatusError struct {
	ErrStatus metav1.Status
}

type APIStatus interface {
	Status() metav1.Status
}

var _ error = &StatusError{}

// Error implements the Error interface.
func (e *StatusError) Error() string {
	return e.ErrStatus.Message
}

func (e *StatusError) Status() metav1.Status {
	return e.ErrStatus
}

func (e *StatusError) DebugError() (string, []interface{}) {
	if out, err := json.MarshalIndent(e.ErrStatus, "", "  "); err == nil {
		return "server response object: %s", []interface{}{string(out)}
	}
	return "server response object: %#v", []interface{}{e.ErrStatus}
}
// error包下还可以这样定义一堆error
var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")

这个包下好友很多check函数,对于一直错误条件的check,可以直接返回bool类型。

func IsBadRequest(err error) bool {
	return ReasonForError(err) == metav1.StatusReasonBadRequest
}
  1. 编写代码时,最后通过谓语句判断直接return,外层统一处理,比如:
// Kubernetes 源码
cmdutil.CheckErr(options.RunCreate(f, cmd))

统一在CheckErr()中处理。

异常panic

直接看一个示例:

func main() {
	B()
	fmt.Println("会执行.")
}
func A()  {
	fmt.Println("func A : do somethings")
	// 出现问题,抛出异常(panic)
	panic("not expect error")
	fmt.Println("不会执行.")

}
func B()  {
	// defer 在前
	defer func(){
		if r := recover(); r != nil {
			log.Println("捕获panic:",r)
		}
	}()
	A()
	fmt.Println("不会执行.")
}

func A : do somethings 会执行. 2018/03/16 11:49:20 捕获panic: not expect error

执行函数A,出现严重问题、致命错误时,使用panic。panic用于停止当前控制流程,并报告一个运行时恐慌/异常(panic)。之后的语句不会被执行,直接把控制权交给函数调用方,直到goroutine调用栈的最顶层。执行B,因为A出现panic,所以后面的语句不会执行,但是defer会执行,从而捕获panic。在main函数中,panic不再出现,所有B函数调用后的语句会被执行。

总结:

  1. panic 会根据函数的调用顺序逐层传递;
  2. panic 仅限于当前协程(routine),子协程抛出 panic,父协程无法捕获;
  3. 虽然子协程 panic 父协程无法捕获,但子协程的 panic 会导致父协程 crash(所以子协程最好捕获 panic),想要父协程获取子协程的 panic 信息,可以在子协程 recover 之后用 channel 传递出去;

参考:
[1]. go-best-practices-error-handling
[2]. error-handling-and-go
[3]. 错误处理
[4]. juju/errors
[5]. go-defer-panic-reover

CONTENTS