1. 缘起
设想这样一个需求。go 服务器程序收到一个请求,需要在数据库里新增一批订单。
要求是:
- 如果这批订单都创建成功,则直接返回「成功」响应。
- 如果这批订单任何一个创建失败,则已创建的订单需要全部撤销,再返回「失败」响应。
那么要怎么写这个逻辑比较好呢?
下面列出设计的思考过程。猛击此处跳转到最终方案。
2. 用 defer 是不行的
go 的 defer 关键字常用于处理「资源获取即初始化(Resource Acquisition Is Initialization, RAII)」。
对于在同一个函数里创建和释放的资源(或资源集合),defer 已经足够了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func foo() error {
var fds []*os.File
if f, err := os.Open("a.txt"); err != nil {
return err
} else {
fds = append(fds, f)
defer f.Close()
}
if f, err := os.Open("b.txt"); err != nil {
return err
} else {
fds = append(fds, f)
defer f.Close()
}
// do something with fds
return nil
}
|
但是,如果要求「资源创建成功时,返回资源,而不是立刻使用它们」,defer 会很难用。因为被 defer 的代码无论整个函数成功还是失败都会被执行,而这里希望如果一切顺利,被 Open 的资源不被 Close。
3. 手工判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func foo() ([]*os.File, error) {
var fds []*os.File
if f, err := os.Open("a.txt"); err != nil {
return nil, err
} else {
fds = append(fds, f)
}
if f, err := os.Open("b.txt"); err != nil {
for _, f := range fds {
f.Close()
}
return nil, err
} else {
fds = append(fds, f)
}
// ... more os.Open
return fds, nil
}
|
这里算是实现了功能。然而,每个失败分支在返回前都要加一段「释放代码」for-range-Close
,容易遗漏。
改进:用 defer
4. 用额外的 bool 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
func foo() ([]*os.File, error) {
var fds []*os.File
ok := true
defer func() {
if !ok {
for _, f := range fds {
f.Close()
}
}
}()
if f, err := os.Open("a.txt"); err != nil {
ok = false
return nil, err
} else {
fds = append(fds, f)
}
if f, err := os.Open("b.txt"); err != nil {
ok = false
return nil, err
} else {
fds = append(fds, f)
}
// ... more os.Open
return fds, nil
}
|
这里代码就好很多,失败时的「释放代码」只要在最前面defer
一下就行。就是每个失败分支都要记得写一遍ok = false
还是挺容易错的。
改进:ok
一开始赋false
,最后返回前才赋true
。
5. defergroup
github.com/seedjyh/defergroup
与其说是「defer group」不如说是「可撤销的 defer」。
用法分三步:
- 构建一个 DeferGroup 对象
dg
,然后立刻defer dg.Do()
。
- 每申请成功一个资源,就把资源的释放函数注册到
dg
。
- 如果中途失败了,
dg.Do()
会执行曾经注册过的所有释放函数。
- 如果最后成功了,就清空
dg
里的所有释放函数。这样,开头被 defer 的操作虽然还是会被调用,但不会调用注册过的释放函数,于是就保留了申请的那些资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func foo() ([]*os.File, error) {
var fds []*os.File
dg := new(DeferGroup)
defer dg.Do()
if f, err := os.Open("a.txt"); err != nil {
return nil, err
} else {
fds = append(fds, f)
dg.Register(func() { f.Close() })
}
if f, err := os.Open("b.txt"); err != nil {
return nil, err
} else {
fds = append(fds, f)
dg.Register(func() { f.Close() })
}
// ... more os.Open
dg.UnregisterAll()
return fds, nil
}
|