Go系列 | Error 处理

974 阅读6分钟

本文收录在专栏Go系列中,更多相关文章可点击前方链接阅读。

前言

在 Go 开发中, Error 的处理一直都是一门学问。不管在各类工具库还是开源项目中,我们总能看到各种各样的错误处理”流派“。本文总结了一些常见的错误处理思路,希望对你有帮助。

错误分支

在 Go 开发中,Error 处理是不可避免的。但在写代码的时候,主线代码应该是我们的主要逻辑,Error 的处理推荐用缩进放在一个“子分支”中。参考下方例子:

// 主线逻辑
value, err := doSomething()
if err != nil {
    // 处理错误
}
// 继续主线逻辑
value, err := doSomething()
if err != nil {
    // 处理错误
}
// 继续主线逻辑
// ......

而下面的代码是没有问题的,但不推荐

// 主线逻辑
value, err := doSomething()
if err == nil {
    // 处理逻辑
}else{
	// 处理错误
}

尽可能少写代码

Go 的 Error 设计决定了开发者需要处理大量的错误判断。这会不可避免地增加开发者的代码量。此时,有效降低代码量的手段就显得十分重要了。但通用的这种手段并不存在,开发者需要根据实际业务以及调用的方式优化自己的代码。下列是一部分代码常见,你可以参考一下:

1.尽量直接返回 error

func SomeFunc(r *Request) error {
    // 调用函数
    err := doSomething()
    // 判断错误
    if err != nil {
        return err
    }
    return nil
}

观察上方代码,我们在调用函数获得 err 之后,并没有对其作任何处理。这种情况,我们可以把 err 直接返回。

func SomeFunc(r *Request) error {
    return doSomething()    
}

注意,这种优化仅限于我们对 doSomething() 返回的 err 没有进一步处理的情况。假如你需要对 doSomething() 返回的 err 有特殊处理,这种优化无法执行了。

2.改用更合适的方法

假如我们需要写一个记录文本行数的方法,我们也许会写出:

func CountLines(r io.Reader)(int,error){
    var (
        br = bufio.NewReader(r)
        lines int
        err error
    )

    for {
        _, err = br.ReadString('\n')
        lines++
        if err != nil {
            break
        }
    }

    if err != io.EOF {
        return 0, err
    }
    return lines, nil
}

上方代码并没有问题,代码量适中。但假如我们了解 bufio.NewScanner() 方法,代码可以优化为:

func CountLines(r io.Reader)(int,error){
	sc := bufio.NewScanner(r)
    lines := 0

    for sc.Scan() {
        lines++
    }

    return lines, sc.Err()
}

优化后的代码量减少了,但业务逻辑依旧可以实现。这种方式的优化要求开发者有比较广的库使用经验,要求在遇到对应业务时,能想起最合适的方法。

3.封装处理

如果我们的代码几乎都在处理由同一个函数返回的 error ,此时我们便可以考虑把这个函数和对应的错误处理封装起来。如下方示例:

// 假如 obj 实例下有 a 方法
value, err := obj.a()
if err != nil {
    // 错误处理
}

value, err := obj.a()
if err != nil {
    // 错误处理
}
// ......

观察上方代码,我们多次使用了 obj.a() 并进行了错误处理。此时我们就可以考虑把 obj 封装一层,代码如下:

// 定义一个新的obj
type newObj struct {
    obj Obj
    err error // 保存 error
}

func (n *newObj) a()(int, error){
    // 判断是否以及出错
    if n.err != nil {
        return 0, n.err
    }
    // 正常执行 ojb.a()
	var value int
    value, n.err = n.obj.a()
    return value, nil
}

func main(){
    // .....
    // 把原来 obj 套一层
    nObj := &newOjb{obj:obj}
    // 执行新示例的 a 方法
	value, err := nObj.a()
	value, err := nObj.a()
	// ......
}

在封装了新一层 obj 之后,原来调用 obj 的方法都可以改用调用新 obj 的方法。由于错误处理已经在封装方法中执行了,后续执行方法就不需要再处理 err 了。这种优化方式是有阈值的,假如 obj.a 本来执行的次数不多,就没有必要封装了。因为封装本身也占一定的代码量,开发者需要自行评估。

每个错误只处理一次

Go 推荐在函数调用时把 Error 作为参数返回。这就会让开发者遇到一个困惑:不知道什么时候应该输出 Error 。

funcB () (int,error){
	value, err := doSomething()
	if !err {
  	// 处理错误
  	return 0, err
  }
	return value, nil
}

funcA (){
	value, err := funcB()
	if !err {
  	// 处理错误
  }
}

如上方例子在执行 funcB 时,我们在内部处理了错误。但在 funcA 执行时,我们又处理了一次 funcB 返回的错误。假如我们这里的错误处理是打印日志,这样程序在执行时,同一条错误我们会看到大量的相关日志。

因此我们推荐在函数中,每个错误只处理一次,即返回错误或解决错误。如果函数能解决这个错误,就把错误解决然后不返回 error 。假如不能解决,就把 error 返回由上层作判断。假如错误一直没被处理,到了顶层就触发 panic 。

funcB () (int,error){
	value, err := doSomething()
	// 不作处理,直接返回
	if !err {
  	return 0, err
  }
	return value, nil
}

funcA (){
	value, err := funcB()
	if !err {
  	// 处理错误
  }
}

加入错误信息

结合上方的观点,由于我们每个错误只处理一次,最终错误在日志输出时,可能已经不在 error 产生的函数内了。此时开发者无法根据日志定位问题,因此我们需要给错误日志加入更多信息。

通常我们推荐在错误中加入当前的函数名或者业务流程名。更具体地可以加入文件名和行号。

包装错误

想要实现错误信息的添加,我们可以使用错误包装的方法。

func main() {
	err1 := errors.New("error1")
	err2 := fmt.Errorf("error2: [%w]", err1)
	fmt.Println(err2)
	fmt.Println(errors.Unwrap(err2))
}

观察上方代码,err2 通过 fmt.Errorf 结合 %w 关键字实现了错误信息的包装。通过这种方式,我们就可以实现对错误在调用链上的追踪。

日志建议

在开发过程中,开发者由于开发时间太短而无法仔细的解决程序错误。通常会选择日志输出的方式先解决问题。

而每个开发者对日志的规范都不一样,这里提出一点建议,大家可以参考。

无效日志

日志记录与错误无关且对调试没有帮助的日志,称为“噪音”。此类日志为无效日志,应该尽量避免。

日志推荐

开发者在输出日志时,可以参考下方要求:

  • 所有错误都要被日志记录到,且日志中包含了解决办法(也可以是建议)。
  • 应用程序处理错误,保证100%完整性。
  • 已经打印过的错误,之后不要再重复

pkg/errors

最后,Go 的 Error 库一直在迭代。我们相信它会做得越来越好。这里再推荐一个第三方的库,它是专门处理 Error 的,为很多业务场景提供了解决方案。有兴趣的朋友可以自行查阅,由此是 GO 1.13 之前版本的开发者,相信对你帮助很大。

pkg/errors 库文档:pkg.go.dev/github.com/…

总结

今天我们整理一些列在 Go 开发中,对 Error 处理的思路与技巧。Error 的处理在 Go 开发中一直是一个不可忽视的部分,有意识地约束错误的处理,不仅有利于大型项目的开发,还能帮助小项目的发展。

如果你觉得本文对你有一点帮助,麻烦给我点个赞吧~~ 谢谢