golang error 设计

306 阅读4分钟
原文链接: icell.io

对于一开始写 go 代码的开发者来说,可能最受不了的一件事情就是对 error 的处理,之前听一个同事说他写到至今 go 代码,其中有将近 40% 的代码都是 if err != nil {...},确实让人有点崩溃。写 go 写了一段时间之后觉得也没什么,起码觉得这样看起来也挺清晰的。

在 go 中,error 是一个 interface{} 类型,它只包含一个 Error() 方法,类型声明如下:


type error interface {
    Error() string
}

这样想要自定义一个 error 就很简单,比如 net package 中定义 DNSError 就是按照如下方法定义的:


type DNSError struct {
    Err         string // description of the error
    Name        string // name looked for
    Server      string // server used
    IsTimeout   bool   // if true, timed out; not all timeouts set this
    IsTemporary bool   // if true, error is temporary; not all errors set this
}
 
func (e *DNSError) Error() string {
    if e == nil {
        return ""
    }
    s := "lookup " + e.Name
    if e.Server != "" {
        s += " on " + e.Server
    }
    s += ": " + e.Err
    return s
}

而在编写 go 代码时,最普遍使用的 error 实现是 errors package 中的,其中的代码也非常简单,只有几行:


package errors

func New(text string) error {
	return &errorString{text}
}

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

在实际开发过程中,我们处理错误的时候如果仅仅是将报错展示出去那就很简单,只需要通过调用 Error() 方法就可以拿到报错的描述,但是大部分情况下,我们往往需要知道这是属于哪种类型的报错,以便做不同的处理,这时候便有好几种解决方案。

直接进行匹配

预先使用 errors packageNew(text string) 方法定义好 error,然后将获取到的 error 同已经定义好的 error 做比较,因为最终获取到的 error 和已经定义好的 error 其实都是遵循 error interfacestruct 类型,而 golang 中的 struct 是可以进行比较的。比如在 gorm 这个数据库 ORMpackage 中,就使用了 errors package 来定义了 ErrRecordNotFound


// ErrRecordNotFound returns a "record not found error". Occurs only when attempting to query the database with a struct; querying with a slice won't return this error
var ErrRecordNotFound = errors.New("record not found")

说到这里,gorm 中有一个我觉得很有意思的一个设计,因为在数据库执行过程中会执行多条语句,此时可能会有多条 error 信息,gorm 的处理是定义了 type Errors []error 这样一个类型,然后让它遵循 error interface,通过这样达到一个一条 error 包含多条 error 的目的。

通过 interface 的类型转换

有时候我们需要自己定义 error 类型以满足开发需求,比如除了仅仅知道报错信息之外,我们还需要针对报错增加错误码,因为 error 是一个 interface,所以实现起来很简单。在具体开发过程中,拿到一条 error 对它进行类型转换,转换成自定义的 error 就可以做我们想做的操作了。

我们还是用 gorm 来举例,上面我们说 gorm 中定义了一个 Errors 类型,它遵循了 error interface,并实际上是一个 []error 类型,所以我们拿到一条 error 可以对它做类型转换,比如下面这个方法:


// IsRecordNotFoundError returns true if error contains a RecordNotFound error
func IsRecordNotFoundError(err error) bool {
	if errs, ok := err.(Errors); ok {
		for _, err := range errs {
			if err == ErrRecordNotFound {
				return true
			}
		}
	}
	return err == ErrRecordNotFound
}

Go 2.0 Draft

虽说 go 目前的 error 处理写起来简单粗暴,看代码看起来也挺简单,但是毕竟这也都是 9102 年了,这种对错误的处理总觉得很落后,好在 Google 在 go 2 草案中开始对此进行了改进,增加了 checkhandle 两个关键字来对 error 进行处理,在这里简单介绍一下。

比如说我们需要实现一个 copyFile 的操作,根据 go 2 的草案,增加 checkhandle之后,就很简单了:


func CopyFile(src, dst string) error {
	handle err {
		return err
	}

	r := check os.Open(src)
	defer r.Close()

	w := check os.Create(dst)
	handle err {
		w.Close()
		os.Remove(dst)
	}

	check io.Copy(w, r)
	check w.Close()

	return nil
}

其中,在需要检查 error 的操作中增加 check,然后配合 handle 代码块来进行对 error 的处理,handle 代码块可以有多个,会按照从下往上的顺序以此执行,这看上去感觉有点像 try-catch 的处理,比现在的错误处理写起来要爽很多。