前言

这是Go十大常见错误系列的第5篇:go语言Error管理。素材来源于Go布道者,现Docker公司资深工程师Teiva Harsanyi

本文涉及的源代码全部开源在:Go十大常见错误源代码,欢迎大家关注公众号,及时获取本系列最新更新。

场景

Go语言在错误处理(error handling)机制上经常被诟病。

在Go 1.13版本之前,Go标准库里只有一个用于构建error的errors.New函数,没有其它函数。

pkg/errors包

由于Go标准库里errors包的功能比较少,所以很多人可能用过开源的pkg/errors包来处理Go语言里的error。

比较早使用Go语言做开发,并且使用pkg/errors包的开发者也会犯一些错误,下文会详细讲到。

pkg/errors包的代码风格很好,遵循了下面的error处理法则。

An error should be handled only once. Logging an error is handling an error. So an error should either be logged or propagated.

翻译成中文就是:

error只应该被处理一次,打印error也是对error的一种处理。所以对于error,要么打印出来,要么就把error返回传递给上一层。

很多开发者在日常开发中,如果某个函数里遇到了error,可能会先打印error,同时把error也返回给上层调用方,这就没有遵循上面的最佳实践。

我们接下来看一个具体的示例,代码逻辑是后台收到了一个RESTful的接口请求,触发了数据库报错。我们想打印如下的堆栈信息:

1
2
3
unable to serve HTTP POST request for customer 1234
 |_ unable to insert customer contract abcd
     |_ unable to commit transaction

假设我们使用pkg/errors包,我们可以使用如下代码来实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func postHandler(customer Customer) Status {
	err := insert(customer.Contract)
	if err != nil {
		log.WithError(err).Errorf("unable to serve HTTP POST request for customer %s", customer.ID)
		return Status{ok: false}
	}
	return Status{ok: true}
}

func insert(contract Contract) error {
	err := dbQuery(contract)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)
	}
	return nil
}

func dbQuery(contract Contract) error {
	// Do something then fail
	return errors.New("unable to commit transaction")
}

函数调用链是postHandler -> insert -> dbQuery

  • dbQuery使用errors.New函数创建error并返回给上层调用方。
  • insertdbQuery返回的error做了一层封装,添加了一些上下文信息,把error返回给上层调用方。
  • postHandler打印insert返回的error。

函数调用链的每一层,要么返回error,要么打印error,遵循了上面提到的error处理法则。

error判断

在业务逻辑里,我们经常会需要判断error类型,根据error的类型,决定下一步的操作:

  • 比如可能做重试操作,直到成功。
  • 比如可能直接打印错误日志,然后退出函数。

举个例子,假设我们使用了一个名为db的包,用来做数据库的读写操作。

在数据库负载比较高的情况下,调用db包里的方法可能会返回一个临时的db.DBError的错误,对于这种情况我们需要做重试。

那就可以使用如下的代码,先判断error的类型,然后根据具体的error类型做对应的处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func postHandler(customer Customer) Status {
	err := insert(customer.Contract)
	if err != nil {
		switch errors.Cause(err).(type) {
		default:
			log.WithError(err).Errorf("unable to serve HTTP POST request for customer %s", customer.ID)
			return Status{ok: false}
		case *db.DBError:
			return retry(customer)
		}

	}
	return Status{ok: true}
}

func insert(contract Contract) error {
	err := db.dbQuery(contract)
	if err != nil {
		return errors.Wrapf(err, "unable to insert customer contract %s", contract.ID)
	}
	return nil
}

上面判断error的类型使用了pkg/errors包里的errors.Cause函数。

常见错误

对于上面的error判断,一个常见的错误是如下的代码:

1
2
3
4
5
6
7
switch err.(type) {
default:
  log.WithError(err).Errorf("unable to serve HTTP POST request for customer %s", customer.ID)
  return Status{ok: false}
case *db.DBError:
  return retry(customer)
}

可能的错误在哪里呢?

上面代码示例里对error类型的判断使用了err.(type),没有使用errors.Cause(err).(type)

如果在业务函数调用链中有一个环节对*db.DBError做了封装,那err.(type)就无法匹配到*db.DBError,就永远不会触发重试。

推荐阅读

开源地址

文章和示例代码开源在GitHub: Go语言初级、中级和高级教程

公众号:coding进阶。关注公众号可以获取最新Go面试题和技术栈。

个人网站:Jincheng’s Blog

知乎:无忌

福利

我为大家整理了一份后端开发学习资料礼包,包含编程语言入门到进阶知识(Go、C++、Python)、后端开发技术栈、面试题等。

关注公众号「coding进阶」,发送消息 backend 领取资料礼包,这份资料会不定期更新,加入我觉得有价值的资料。还可以发送消息「进群」,和同行一起交流学习,答疑解惑。

References