Golang下的Error

感觉error确实没啥可说的,这个简单到极致的package总共也不超过10行有效代码。而且常用的fmt也提供了很方便的返回error的方法:

// Package errors implements functions to manipulate errors.
package errors

// New returns an error that formats as the given text.
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
}

自定义error

error设计的如此简单,导致其判断错误类型就比较麻烦。比如我想判断MySQL的报错是否由主键冲突导致,我可以这样处理:

const PrimaryKeyDuplicateCode = "1062"
if strings.Contains(err.Error(), PrimaryKeyDuplicateCode) {
	//commands
}

这样的判断逻辑,如果仅是用于特殊情况,还勉强可以接收。但如果你要整个项目都使用这种形式的话,就会觉得精神崩溃,心理无法承受(反正我是这样感觉的)。所以,我们要自定义实现一个Error结构。当然,这样搞还有syscall这个package

实现自定义的Error非常简单,我们要Error里面包含状态码、错误描述、上下文数据,然后实现error接口就可以。

type error interface {
	Error() string
}

下面便是我们自定义的errorData用来存储错误的上下文信息。当然,我们其实可以为Data专门定义新的结构类型,由它来封装数据的操作。然后,我们实现了Error方法,以此来实现error接口。该方法返回json编码的字符串,如果json编码失败,则fmt输出。

type CustomError struct {
	Code int
	Msg  string
	Data map[string]interface{}
}

unc (e *CustomError) Error() string {
	data, err := json.Marshal(e)
	if err == nil {
		//return fmt.Sprintf("%v", e.Msg)
		return fmt.Sprintf("%v", e)
	}
	return string(data)
}

你有没有发现这段代码隐藏了一个大坑!fmt.Sprintf("%v", e)这段代码背后到底执行了怎样的操作。下面便是可能会出现的错误:

runtime: goroutine stack exceeds 1000000000-byte limit
fatal error: stack overflow

无限的递归最终导致栈溢出,当err != nil的时候,便会无限次的调用Error方法,最终导致了栈溢出。结果就是程序彻底崩溃了。下面来看fmt.Sprintf的方法实现:

// If a string is acceptable according to the format, see if
// the value satisfies one of the string-valued interfaces.
// Println etc. set verb to %v, which is "stringable".
switch verb {
case 'v', 's', 'x', 'X', 'q':
	// Is it an error or Stringer?
	// The duplication in the bodies is necessary:
	// setting handled and deferring catchPanic
	// must happen before calling the method.
	switch v := p.arg.(type) {
	case error:
		handled = true
		defer p.catchPanic(p.arg, verb)
		// 如果是error类型,调用其Error方法
		p.fmtString(v.Error(), verb)
		return

	case Stringer:
		handled = true
		defer p.catchPanic(p.arg, verb)
		p.fmtString(v.String(), verb)
		return
	}
}

判断Error是否为nil

go中相当常见的判断,估计就是err != nil了。它遵循提前退出的原则,当err不为空是,函数体就应立即中断,然后返回(当然也有特殊的了,就比如io.EOF)。但如果你没有好好推敲过err != nil这个比较逻辑的话,很可能就会吃点小亏。

通过一个简化版的例子,来说明问题。首先,声明一个函数,返回自定义的error。当errSwitch设置为false时,返回nil

func returnCustomError(errSwitch bool) *CustomError {

	if errSwitch == true {
		return &CustomError{
			Data: make(map[string]interface{}, 0),
		}
	}

	return nil
}

之后再声明另外一个函数,返回error接口类型,内部调用returnCustomError函数:

func returnOfficialError() error {
	return returnCustomError(false)
}

func main() {
    //比较
	if returnOfficialError() != nil {
		fmt.Println("err is not equal to nil")
	} else {
		fmt.Println("err is equal to nil")
	}
}

//output
//err is not equal to nil

是不是挺奇怪的,我明明返回了一个nil,但最后判断的结果却!= nil。问题出在interface类型的比较上,它会比较interface typeinterafce value,只有两者均为nil,最终结果才为nil

interface比较

Go语言中,变量均会被初始化为预定义的零值,interface也不例外。但interface的零值却由两部分组成:dynamic typedynamic value,只有两者均为nil,最终结果才为nil

从上面都示例也可以看出,interface是可以比较的。所以,interface类型也可以作为map类型的key值。但如果interface中的dynamic type本身是不可比较的,比如slicemapfunction,强行比较的话,就会引起panic。因此,在比较interface之前,一定要确定dynamic type是可以比较的。

总结

在项目中,函数的error返回类型尽量要做到统一,要么所有的函数均返回error interface类型(建议),要么返回自定义的类型。这样可以避免上述的情况。在处理特殊error的类型时,使用断言来做特殊处理。

//使用断言来判断错误的类型
if err, ok := err.(*CustomError); ok {
    
}