Try(https://github.com/dsnet/try) go1.18 后一种新的 panic 处理方式
错误处理 —— Error Handling
Try 在 Golang 中实现了一种极简的错误处理方式。
不过这里需要明确一点使用前提,必须是 Go1.18 以上的泛型版本才可以使用此库。
入门
回顾 Golang 标准错误处理流程
作为比较,先放一下 Golang 中标准的错误处理流程。
type Duck struct {
Age int
}
func main() {
duck := Duck{Age:1}
bs, err := json.Marshal(&duck)
if err != nil {
log.Fatal("marshal duck fail")
return
}
var dummy Duck
err = json.Unmarshal(bs, &dummy)
if err != nil {
log.Fatal("unmarshal duck fail")
return
}
fmt.Printf("dummy: %+v", dummy)
}
标准的 Goland Error Handling 实在是有点繁琐,每次 err 都需要单独判断,频繁的 if 逻辑穿插在业务处理流程当中。
如何能够像类似 Java Try-Catch 一样的错误处理方式呢?
使用 Try 进行错误处理
现在我们尝试使用 Try 来编写以上代码:
type Duck struct {
Age int
}
func main() {
defer try.F(log.Fatal)
duck := Duck{Age:1}
bs := try.E1(json.Marshal(&duck))
var dummy Duck
try.E(json.Unmarshal(bs, &dummy))
fmt.Printf("dummy: %+v", dummy)
}
太干净了!可以看到主流程都非常紧密的连接在一起了,错误处理已经被 Try 给包裹住,不需要再使用烦人的 if err!= nil
语句了。
Try API 解析
全量 API 列表
func E(err error)
func E1(a A, err error) A
func E2(a A, b B, err error) (A, B)
func E3(a A, b B, c C, err error) (A, B, C)
func E4(a A, b B, c C, d D, err error) (A, B, C, D)
func F(fn func(...any))
func Handle(errptr *error)
func HandleF(errptr *error, fn func())
func Recover(fn func(err error, frame runtime.Frame))
通过 API 可以发现 Try 的设计:
- 函数
E()
是核心,主要用来包裹返回参数error
。多个函数E()
是因为需要匹配不同数量的返回值。- 使用了泛型去匹配非 error 类型的变量,所以必须要 Go1.18 以上才可使用
- error 都是在末位,所以函数
E()
只能支持 error 在最后一位的函数返回值作为入参 - 如果需要有更长数量返回值的函数,或 error 位置不在最后一位的函数,可以新增函数
E()
来适配
- 函数
F(fn func(...any))
实际上接收的是一个函数,函数的入参是可变长的 any 类型,“入门”一节中使用的 log.Fatal 就是这类函数 - 函数
Handle(errptr *error)
接收了一个 error 指针,这里的使用方式是,将拦截到的 error 信息可以赋值到 errptr 指针处,后面介绍一个用法 - 函数
HandleF(errptr *error, fn func())
接受了一个 error 指针的同时,也提供了一个处理函数,这个处理函数会在 error 赋值到 errptr 指针后进行,后面补充用法 - 函数
Recover(...)
可以看到接受了一个函数,可以同时处理 error 和栈帧信息
函数 Handler 使用
func f() (err error) {
defer try.Hander(&err)
try.E(...)
...
}
可以看到此处函数 Handler
接收了 函数 f
的返回参数中的 err 变量地址作为入参,此时如果 try.E
中拦截到了 error,会将这个 error 信息赋值到返回参数的 err 变量中,实现了错误的传递。
函数 HandlerF 使用
func f() (err error) {
defer try.HandlerF(&err, func() {
err = fmt.Errorf("f() call err: %w", err)
})
try.E(...)
...
}
函数 HandlerF
的一个功能是将 error 传递到函数 f
的返回参数 err 中;另一个能力是接收一个自定义函数。假定的一个场景是:在这个自定义函数中,对传递的 error 进行一次 wrap,增加上一些额外信息 f() call err:
,这样可以标识错误传递来源是函数f
。
原理
panic and recover
func f() {
defer recover()
panic("^_^")
}
Try 利用了 Golang 提供的 panic & recover 的能力,巧妙的将业务逻辑中的 error 信息包装成 panic,随后被 defer 中的 recover 捕获,从而完成中断业务处理,返回错误信息的能力。
以函数 Handler(errptr *error)
举例,代码中对 panic 操作进行了 recover,并且只有 panic 的信息会 error 对象时,才捕获并转移到 errptr 中。
func Handle(errptr *error) {
r := recover()
switch r.(type) {
case nil:
case error:
*errptr = r.(error)
default:
panic(r)
}
}
以上代码存在的问题是,recover 出来的 error 可能并不是 try.E()
函数 panic 抛出的,所以上文定义的 Handler()
执行下来会存在将其他代码片段出现的 panic error 一并处理了。这超出了 Try 职能的范围。所以在 Try 的实现中,实际上是自定义了一个 error 类型—— wrapError
,这样在 switch type 判断中,明确判断是否是 wrapError
即可准确的处理 Try 函数中抛出的 error。
type wrapError struct {
error
pc [1]uintptr
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [email protected]