error 与 try catch 对比
有人说err写得繁琐,而我却觉得更准确,清晰。更明确的说明这个函数可能有错,提示程序猿需要显示处理。
当然我也没觉得java那样的try catch有什么问题,不过现在我更喜欢err。
我们来对比一下两者的优劣,从写法和排错方法两个方面。
写法
在编写被调用函数方两者基本差别不大,一边需要写err 一边需要throw。
在调用方try的方式可以一次捕获多个thower,而err就需要繁琐的写多次。
当然try也有坏处,try会导致代码缩进一层。
排错
try看问题必须看错误堆栈,而我认为堆栈中80%的字符都是无用的。
在另一篇文章中也有此观点
用户和实现者
让错误对终端用户有用并且保持简洁,与让错误对实现者而言信息丰富并且可供分析,二者之间存在矛盾。常常是实现者胜出,而错误变得过于冗余,达到了包含堆栈跟踪或者其他淹没式细节的程度。
Upspin 的错误试图让用户和实现者都满意。报告的错误适度简洁,关注于用户应该觉得有用的信息。但它们还包含内部详细信息,例如方法实现者可以获取诊断信息,但又不会把用户淹没。在实践中,我们发现这种权衡工作良好。
相反,类似于堆栈跟踪的错误在这两方面上都更糟糕。用户没有上下文可以理解堆栈跟踪,而如果服务端错误被传给客户端的话,那么看到堆栈跟踪的实现者会很难看到应该出现的信息。这就是为什么 Upspin 错误嵌套相当于操作跟踪(显示系统元素路径),而不是执行跟踪(显示代码执行路径)。这个区别至关重要。
对于那些堆栈跟踪可能会有用的场景,我们允许使用 “debug” 标签来构建 errors 包,这将会允许打印堆栈跟踪。这个工作良好,但是值得注意的是,我们几乎从不使用这个功能。相反,errors 包的默认行为已经够好了,避免了堆栈跟踪的开销和不堪入目。
那么没有堆栈,怎么确定错误呢?
到底需不需要堆栈
为了解释堆栈信息也能定位问题,我先提出一个概念:
一个函数应该是完善的,确定的。
为了更好理解, 先看例子。
// 糟糕的代码
// 入参模糊, 当发生错误, 必须依赖上层参数才能找到错误.
func Insert(data interface{}) (err error) {
err = mysql.Insert(data)
if err != nil {
return
}
return
}
// 正确的代码
// 确定性: 入参确定
func InsertUser(user *User) (err error) {
// 完善性: 函数应该自己能够判断一个入参是否非法并立刻返回错误
if user == nil {
err = ...
return
}
if user.UserName == "" {
err = ...
return
}
err = mysql.Insert(user)
if err != nil {
return
}
return
}
完善的: 自己管理自己的入参,函数应该自己能够判断一个入参是否非法并立刻返回错误,不要等到最后报一个模棱两可的错。
确定的: 一个函数做一件事情,并且入参和出参是确定的,这样无论多少个调用者来调用这个函数,都不会因为入参不同而需要上层调用者信息来排查问题。
可以看到,排错的简易程度和有误错误堆栈并没有必然关系,和编写代码的人却有必然关系。
error的问题
官方error包的缺陷也很明显,由于没有错误堆栈,如果只有一个string提示的话,当程序比较复杂,层级较多时,我们很难定位到是那一层出错了。
那么又矛盾了,那还是需要错误堆栈的呀?
程序猿至少需要看到两个信息才能排错:
- 错误路线: 当代码层级较多, 或者是微服务之间的调用的时候, 错误路线尤为重要
- 错误发生地: 也就是错误发生的代码位置
如何实现错误路线呢? 可以参考gopkg中errors包的做法: Warp.
至于代码位置可以用runtime.Caller获取.
下面来具体看看代码这么写.
优化error
定义
type Error struct {
code uint32
msg string
where string
}
func (e *Error) Error() string {
return fmt.Sprintf("code = %d ; msg = %s", e.code, e.msg)
}
至于为什么需要code, 稍后再讲.
主要方法有两个
func New(code int, msg string) *Error {
// 获取代码位置, 代码就不贴了, 不是重点.
where := caller(1)
return &{code:code, msg:msg, where: where}
}
主要看Warp方法
Warp为错误添加一个附加信息
, 一般填写操作
.
func Warp(err error, msg string) *Error {
var where string
var code int
switch t:=err.(type){
case *Error:
// 继承where和code
where = t.where
code = t.code
// 拼接上之前的错误
msg = msg + ":: " + t.msg
default:
where = caller(1)
}
return &{code: code, msg: msg, where: where}
}
使用
我们模拟几种报错
// 子数据层
func InsertA(id int) (err error) {
if id == 0 {
err = error.NewCoder(400, "id不能为空")
return
}
return
}
// 数据层
func InsertA(aid int, bid int) (err error) {
err = InsertA(aid)
if err != nil {
// 使用wrap方法为错误添加一个附加信息
err = error.Wrap(err, "InsertA")
return
}
return
}
// controll层
func Main() {
err = InsertAB(a, b)
println(err)
}
最终的错误会是这样:
{
code: 400,
msg: "InsertA:: id不能为空",
where: "/root/go/src/error/error.go:9"
}
如何使用上面的信息(如何打印, 如何返回给用户)就由你来定了.
其中msg也反映了调用的路径. 这对于开发者有用, 但是用户并不关系, 如果要返回给用户看的话, 最好处理下msg:
很简单, split(":: "), 并获取最后一段即可. 比如这里, 用户只需要看到 "id不能为空"
.
code
我使用code来"匹配错误". 在其他包中, 匹配错误的方式有很多:
if err == io.NotExist{
}
if error.Is(err, io.NotExist){
}
当然他们都没有错, 只是使用场景不同, 再更业务的层面, 我推荐使用code去匹配错误, 原因如下
- 当使用微服务架构时, 如果A服务需要匹配B服务的错误, 那么一定需要共享错误, 相比起共享Golang中的错误对象(如 io.NotExist), 我更愿意共享int型的code码.
- 可以使用
段
来区分大概的错误类型, 比如>=500的错误就是系统错误.
最后
当然这个方案并不完美,不过希望能给你带来一点灵感。
对了, 建议仔细阅读在开头提及的一篇文章: Upspin 中的错误处理 —— 来自 Rob Pike, 相信你会得到更多.
网友评论