安全地执行不安全的函数
在日常开发中,可能会遇到一种场景:
开启一个事务去执行某个任务,这个任务的其中一个子任务有可能失败,但是你又不想这个失败影响整个事务,只想打印一下日志。这种场景就可以用上安全函数 SaveFunc。
举个栗子,在修改简历时,我希望能发送一条通知给关注了这份简历的人,而发送通知有可能失败,甚至引发 panic。
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
26
27
28
29
30
| func UpdateResume(rid Resume, data ResumeData) bool {
if data == nil {
return false
}
// 启动一个事务更新简历
err := newTransaction(func(){
rid.SetData(data)
SendNotice() // 可能因这个函数 panic 而导致整个 UpdateResume 事务失败
})
if err != nil {
log.Printf("transaction fail. Cause: ", err)
return false
}
log.Printf("transaction success.")
return true
}
func SendNotice() {
if Search(ctx) == nil {
panic("can not get resume.")
}
notice := new(Notice)
if !notice.Send() {
log.Println("sent notice fail.")
}
log.Println("sent notice success.")
}
|
在发送通知的函数 SendNotice()
如果 Search 不到资源则会直接 panic,这样会影响到整个事务失败回滚,从而 UpdateResume()
也失败。
这样显然不合理,总不能因为你发送通知失败就不让我更新简历吧?所以对于这样的场景,我们要对 SendNotice()
做安全处理。
SaveFunc#
那么就轮到主角 SaveFunc()
登场了:
1
2
3
4
5
6
7
8
9
10
11
12
13
| import "github.com/pkg/errors"
type UnsaveFunc func () error
func SaveFunc(fn UnsaveFunc) (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Wrap(r)
}
}()
return fn()
}
|
SaveFunc()
其实非常简单,函数名你可以换成你自己喜欢的名字。
它仅仅做一件非常普通的事情,把不安全的函数 fn 传递进来,在这里面执行,即使 fn 发生 panic 也会被 defer 中捕获,然后 recover,使得程序不会被中断。
这样加了一层用于兜底,就不怕不安全的函数会影响外层事务的执行了。
具体使用如下:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| type UnsaveFunc func () error
func SaveFunc(fn UnsaveFunc) (err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Wrap(r)
}
}()
return fn()
}
func UpdateResume(rid Resume, data ResumeData) bool {
if data == nil {
return false
}
// 启动一个事务更新简历
err := newTransaction(func(){
rid.SetData(data)
if e := SaveFunc(SendNotice()); e != nil {
log.Println(e)
}
})
if err != nil {
log.Printf("transaction fail. Cause: ", err)
return false
}
log.Printf("transaction success.")
return true
}
// 给不安全函数增加一个返回值 error 以符合 UnsaveFunc 定义
func SendNotice() error {
if Search(ctx) == nil {
panic("can not get context.")
}
notice := new(Notice)
if !notice.Send() {
return errors.New("sent notice fail.")
}
return nil
}
|
上面的代码将原本的例子中不安全的函数 SendNotice()
增加了一个返回值 error 以符合 UnsaveFunc 的定义。
然后在调用的地方 UpdateResume()
用 SaveFunc(SendNotice)
将函数传递进去。这样即使 SendNotice()
发生 panic 也会被 SaveFunc 处理掉,不会影响 UpdateResume()
。
但这样有三个不好的地方:
- 它破坏了原本的
SendNotice()
,是一种侵入性的改造; - 它限定了不安全函数
SendNotice()
不能携带参数; - 它限定了不安全函数
SendNotice()
不能返回结果。
这对这几个问题,我们可以用闭包来解决。
使用闭包替代侵入性改造#
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| func SendNotice() {
if Search(ctx) == nil {
panic("can not get context.")
}
notice := new(Notice)
if !notice.Send() {
log.Println("sent notice fail.")
}
log.Println("sent notice success.")
}
func ClosureSendNotice() UnsaveFunc {
return func () error {
SendNotice()
return nil
}
}
func UpdateResume(rid Resume, data ResumeData) bool {
if data == nil {
return false
}
// 启动一个事务更新简历
err := newTransaction(func(){
rid.SetData(data)
if e := SaveFunc(ClosureSendNotice()); e != nil {
log.Println(e)
}
})
if err != nil {
log.Printf("transaction fail. Cause: ", err)
return false
}
log.Printf("transaction success.")
return true
}
|
我们使用一个闭包返回一个 UnsaveFunc,将不安全的函数 SendNotice()
放在里面,这样就不会造成侵入性改造,也不需要更改 UnsaveFunc 的定义了。
这样就解决第一个问题。
使用闭包解决参数问题#
为了说明这个问题,我们先让 SendNotice()
需要接收一个参数。
1
2
3
4
5
6
7
8
9
10
11
12
| func SendNotice(rid Resume) {
if Search(ctx) == nil {
panic("can not get context.")
}
notice := new(Notice)
notice.rid = rid
if !notice.Send() {
log.Println("sent notice fail.")
}
log.Println("sent notice success.")
}
|
这里其实也很简单
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| func SendNotice(rid Resume) {
if Search(ctx) == nil {
panic("can not get context.")
}
notice := new(Notice)
notice.rid = rid
if !notice.Send() {
log.Println("sent notice fail.")
}
log.Println("sent notice success.")
}
func ClosureSendNotice(rid Resume) UnsaveFunc {
return func () error {
SendNotice(rid)
return nil
}
}
func UpdateResume(rid Resume, data ResumeData) bool {
if data == nil {
return false
}
// 启动一个事务更新简历
err := newTransaction(func(){
rid.SetData(data)
if e := SaveFunc(ClosureSendNotice(rid)); e != nil {
log.Println(e)
}
})
if err != nil {
log.Printf("transaction fail. Cause: ", err)
return false
}
log.Printf("transaction success.")
return true
}
|
这里其实也很简单,只需要给闭包函数写上 SendNotice()
需要的参数,然后闭包中的 SendNotice()
直接调用就行。
参数会栈逃逸到堆上,这点不需要担心作用域的问题。
使用闭包解决返回值问题#
这个问题好像跟最初的场景相悖了。
如果不安全函数 SendNotice()
有返回值,那么在 UpdateResume()
中的事务有两种情况:
- 事务中 不需要 用到
SendNotice()
的返回值 - 事务中 需要 用到
SendNotice()
的返回值
1
2
3
4
5
6
7
8
9
10
11
12
| func UpdateResume(rid Resume, data ResumeData) bool {
......
err := newTransaction(func(){
rid.SetData(data)
if e := SaveFunc(ClosureSendNotice(rid)); err != nil {
log.Println(e)
}
})
......
}
|
如果不需要用到,那么就不需要考虑返回值的事情了。
如果需要用到,说明事务中依赖 SendNotice()
的返回值,这样的话,SendNotice()
失败不就意味着整个事务失败了吗。那还费劲心机搞个 SaveFunc 干嘛?
所以返回值这点,可以不处理。
照目前这个场景是这种结论,我还没想出其他场景。
如果非要处理返回值这个问题也不是没有办法,但是要连同 SaveFunc 一起改造,也很不正确。
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| type UnsaveFunc func () (interface{}, error)
func SaveFunc(fn UnsaveFunc) (arg interface{}, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Wrap(r)
}
}()
return fn()
}
func SendNotice() bool {
if Search(ctx) == nil {
panic("can not get resume.")
}
notice := new(Notice)
if !notice.Send() {
log.Println("sent notice fail.")
return false
}
log.Println("sent notice success.")
return true
}
func ClosureSendNotice() UnsaveFunc {
return func () (interface{}, error) {
res := SendNotice()
if !res {
return res, errors.New("fail.")
}
return res, nil
}
}
func UpdateResume(rid Resume, data ResumeData) bool {
if data == nil {
return false
}
// 启动一个事务更新简历
err := newTransaction(func(){
rid.SetData(data)
res, e := SaveFunc(ClosureSendNotice(rid))
if e != nil {
log.Println(e)
}
log.Println(res)
})
if err != nil {
log.Printf("transaction fail. Cause: ", err)
return false
}
log.Printf("transaction success.")
return true
}
|