Golang下的Error
Posted by 付辉 on Saturday, August 11, 2018 共1668字感觉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
}
下面便是我们自定义的error,Data用来存储错误的上下文信息。当然,我们其实可以为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 type和interafce value,只有两者均为nil,最终结果才为nil。
interface比较
Go语言中,变量均会被初始化为预定义的零值,interface也不例外。但interface的零值却由两部分组成:dynamic type和dynamic value,只有两者均为nil,最终结果才为nil。
从上面都示例也可以看出,interface是可以比较的。所以,interface类型也可以作为map类型的key值。但如果interface中的dynamic type本身是不可比较的,比如slice、map、function,强行比较的话,就会引起panic。因此,在比较interface之前,一定要确定dynamic type是可以比较的。
总结
在项目中,函数的error返回类型尽量要做到统一,要么所有的函数均返回error interface类型(建议),要么返回自定义的类型。这样可以避免上述的情况。在处理特殊error的类型时,使用断言来做特殊处理。
//使用断言来判断错误的类型
if err, ok := err.(*CustomError); ok {
}