切换语言为:繁体
go error处理方案

go error处理方案

  • 爱糖宝
  • 2024-09-05
  • 2045
  • 0
  • 0

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类型,可以携带更多的信息,返回方增加错误信息时更容易(参考下图),且能完成错误和错误值之间的解耦。

go 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的返回形式,而不是盲目跟随一个建议,选择一个“合适”的方案,而非唯一的“最佳”方案。

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.