[go语言]使用defer和有名返回值,实现初始化失败的自动回滚

稍微复杂一些的程序的初始化会涉及到多个模块,任何一个模块初始化失败以后,就应该把已经初始化的其他模块一一回滚。这容易使得初始化的错误处理比较冗长,包含重复代码;或者执行路径跳转不容易读懂,也容易出错。用go语言提供的defer和有名返回值的机制,可以比较容易地解决这个问题,让初始化错误处理简洁清晰。

先看看一种常见的错误处理代码:
func Initialize() error {
var err error
err = init_a()
if err != nil {
return err
}
err = init_b()
if err != nil {
uninit_a()
return err
}
err = init_c()
if err != nil {
uninit_b()
uninit_a()
return err
}
err = init_d()
if err != nil {
uninit_c()
uninit_b()
uninit_a()
return err
}
return nil
}
可以看出来,这样的处理非常的麻烦,uninit_a()这样的语句要写一次又一次。

那么试试用goto:
func Initialize() error {
var err error
err = init_a()
if err != nil {
goto _err
}
err = init_b()
if err != nil {
goto _ua
}
err = init_c()
if err != nil {
goto _ub
}
err = init_d()
if err != nil {
goto _uc
}
_uc:
uninit_c()
_ub:
uninit_b()
_ua:
uninit_a()
_err:
return err
_exit:
return nil
}
这样比前一种写法好一些,不用重复地写uinit_a(),uinit_b()...了。但是用goto也有很明显的缺点:
  1. label太多,看的眼花
  2. 使用goto,执行流程一下子跳走了,导致思维中断和跳跃
  3. 变量的初始化全部要放到函数的开始位置,不能放在使用它的地方,不然会编译报错“jumps over declaration”。
这么说goto也不是一个好的方案。有人说可以定义一个函数数组,在循环里做初始化和出错回滚:
func Initialize() error {
inits := []func() error {init_a, init_b, init_c, init_d}
uinits := []func() error {uninit_a, uninit_b, uninit_c, uninit_d}
for i := 0; i < len(inits); i++ {
err := inits[i]()
if err != nil {
for j := i - 1; j >= 0; j-- {
uninit[j]()
}
return err
}
}
return nil
}
这个方案看上去很简洁很漂亮。但是也有一个致命的缺点:它不是一个通用的解决方案。因为init_a(),init_b(),...它们的函数签名不一定一样,甚至有可能有依赖:比如init_b()需要一个变量作为参数,这个变量由init_a()返回。这个时候就没办法统一处理了,所以这个方案也不可行。

但是用defer和有名返回值就可以很好的处理这种问题:
func Initialize() (err error) { // 注意这里定义了一个err做为返回值变量
rollback := func(uninit func()) {
if err != nil {
uninit()
}
}
err = init_a()
if err != nil {
return err
}
defer rollback(uninit_a)
err = init_b()
if err != nil {
return err
}
defer rollback(uninit_b)
err = init_c()
if err != nil {
return err
}
defer rollback(uninit_c)
return init_d()
}
很简洁很美吧?可能还有一个小小的疑问:反初始化函数的参数签名不一致怎么办呢?回答是:
  1. 绝大多数情况下反初始化函数的参数签名都是一样的
  2. 即使个别不一样,在defer的时候也可以用闭包封装一下,没有什么负担。例如:
defer rollback(func(){uninit_b(100)})

就是这么简单。

golang-nut(需要翻墙)上讨论相关问题的时候,得到了Ian Lance Taylor, minux, Nigel Tao以及其他人的热情指点和帮助,非常感谢他们以及热情和充满活力的golang社区。四月份平民指出文中最后一段代码有误(现已修正),在此也表示感谢。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。