error是什么
error是go原生内置的接口类型,用于表示错误,本质上与其他接口类型并无区别。在go的设计哲学中,开发人员需要显式关注和处理error。开发过程中,要时刻关注error值,并进行处理,以提高系统健壮性。其定义如下:
type interface error { Error() string }
在go中,只要实现了Error() string 这个方法,就可以作为错误赋值error接口变量。 目前在go中主要提供了两种生成error变量的方法;分别如下
fmt.Errorf() errors.New()
fmt.Errorf() 返回的是一个包装的错误信息,能够返回错误链。
errors.New() 返回的错误信息较为简单,仅包含错误消息的字符串。
一般作为约定,在方法或者函数中返回error时,会将error置于返回值的末尾,若没有错误时,则直接返回nil即可。
error使用
软件设计,各种准则与设计模式都是围绕着设计出“低耦合”架构而努力,也即降低模块,类,方法等之间的耦合程度。我们应该在使用设计error的过程中,也应该朝着降低调用方与实现方之间的耦合度方向进行设计。 为此,我们在使用的过程中,可以以下方面考虑设计考虑。
准则
首先我们可以先考虑从以下准则进行思考改进:
调用方是否关心错误内容
区分预期错误和非预期错误,并预先定义预期错误
使用fmt.Errorf() 包装错误。
使用errors.Is和 errors.As比较错误
自定义错误,包装复杂信息
error返回
错误的返回,由实现者处理,实现者需要根据调用方的需求,选择不同的返回策略。
直接返回:调用方不关心错误值,遇到错误时,就会进入唯一的处理路径。这种情况下,error通过errors.New或fmt.Errorf() 返回即可,无需做太多设计。这种模式下,双方之间的耦合最低,互相之间不需要感知error的具体值,只需要感知有错误发生即可。一般的代码结构如下:
func f1() error { ... return errors.New("some errors") } err := f1() //调用方不关心err的具体内容,只要是error就处理。 if err != nil { ... return err }
定义预期错误:调用方关心错误值,根据错误值进入不同的处理路径。此时,实现方可提前定义好预期错误并返回,暴露给调用方使用。此时,双方对错误值产生了耦合,实现方通过提前定义预期错误,降低修改错误值可能造成的影响。一般代码结构如下:
// 提前定义预期错误 var ( Err1 = errors.New("err1...") Err2 = errors.New("err2...") ) //调用方关注错误 switch err { Err1: ... Err2: ... }
一般约定预期错误定义格式为ErrXXX
自定义错误:通过自定义错误,方便错误携带更多的信息,还可以解耦错误值和错误类型,使用方可以利用错误类型执行不同的业务逻辑,而无需关注错误值。如go(1.21版本)中定义了OpError
type OpError struct { Op string Net string ... }
自定义error类型,可以携带更多的信息,返回方增加错误信息时更容易(参考下图),且能完成错误和错误值之间的解耦。
对于自定义错误的设计,我们可以从以下几个方面思考:
实现者不需要做额外的工作,只需要专注于处理包自身的错误
调用者知道发生了什么错误
调用者能够决定如何处理这个错误
调用者能够了解为什么发送这个错误
error比较
直接比较;直接比较常用,一般就是 if err != nil 或者 if err == Err1 这样,直接比较目前还是主流方式,能覆盖大部分使用场景。
errors.Is : 主要用于比较错误值,类似于 if err == Err1,与其不同的是,如果比较返回的error是一个包装类型时,errors.Is会在错误链上与所有的包装错误进行比较,直至最后找到一个匹配值。如:
type Err struct { } func (e *Err) Error() string { return "Err!" } func main() { e := &Err{} e1 := fmt.Errorf("Err1: %w", e) e2 := fmt.Errorf("Err2: %w", e1) fmt.Println(errors.Is(e2, e)) }
示例中error.Is逐层值比较,最后找到e2中与e的匹配error,最后打印true;
errors.As:类似于error的类型断言 if e,ok := err.(Err1) ; 不同是会在错误链上,将所有的包装错误类型进行比较,并将第一个类型匹配的error,赋值到第二个参数值。如:
type Err struct { e string } func (e *Err) Error() string { return e.e } func main() { e := &Err{"my err1"} e1 := fmt.Errorf("Err1: %w", e) e2 := fmt.Errorf("Err2: %w", e1) var e3 *Err fmt.Println(errors.As(e2, &e3)) fmt.Println(e == e3) }
示例中errors.As逐层进行类型匹配,最后匹配到与e3类型匹配的e,并将e赋值给e3。最后会打印两个true。
思考
go编程中,关于error处理,常听到 “Make your errors clear with fmt.Errorf, don not just leave them bare.” 的建议,鼓励使用fmt.Errorf返回error,而非errors.New。
作为使用者,我们应该做的是根据实际需要来决定的error的返回形式,而不是盲目跟随一个建议,选择一个“合适”的方案,而非唯一的“最佳”方案。