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
}