稍微复杂一些的程序的初始化会涉及到多个模块,任何一个模块初始化失败以后,就应该把已经初始化的其他模块一一回滚。这容易使得初始化的错误处理比较冗长,包含重复代码;或者执行路径跳转不容易读懂,也容易出错。用go语言提供的defer和有名返回值的机制,可以比较容易地解决这个问题,让初始化错误处理简洁清晰。
先看看一种常见的错误处理代码:
func Initialize() error {
var err errorerr = init_a()if err != nil {
}return errerr = 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 errorerr = init_a()if err != nil {
}goto _errerr = init_b()if err != nil {
goto _ua}
err = init_c()
if err != nil {
goto _ub}
err = init_d()
if err != nil {
_uc:goto _uc}
uninit_c()_ub:
uninit_b()
_ua:
uninit_a()
_exit:return err
return nil
}
这样比前一种写法好一些,不用重复地写uinit_a(),uinit_b()...了。但是用goto也有很明显的缺点:
- label太多,看的眼花
- 使用goto,执行流程一下子跳走了,导致思维中断和跳跃
- 变量的初始化全部要放到函数的开始位置,不能放在使用它的地方,不然会编译报错“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
}
deferrollback(uninit_c)
}return init_d()
很简洁很美吧?可能还有一个小小的疑问:反初始化函数的参数签名不一致怎么办呢?回答是:
- 绝大多数情况下反初始化函数的参数签名都是一样的
- 即使个别不一样,在defer的时候也可以用闭包封装一下,没有什么负担。例如:
defer rollback(func(){uninit_b(100)})
在golang-nut(需要翻墙)上讨论相关问题的时候,得到了Ian Lance Taylor,
minux, Nigel
Tao以及其他人的热情指点和帮助,非常感谢他们以及热情和充满活力的golang社区。四月份平民指出文中最后一段代码有误(现已修正),在此也表示感谢。