go中的monkey patch

概述

monkey patch(猴子补丁)是一种在不改变原始源代码的情况下扩展或修改动态语言的运行时代码的方法。许多人认为猴子修补只限于Python等动态语言。但事实并非如此,我们可以在运行时来修改Go函数。主角就是github.com/bouk/monkey

猴子补丁主要有以下几个用处:

  • 在运行时替换方法、属性等
  • 在不修改第三方代码的情况下增加原来不支持的功能
  • 在运行时为内存中的对象增加patch而不是在磁盘的源代码中增加
  • 增加钩子,在执行某个方法的同时执行一些其他的处理,如打印日志,实现AOP等。

Example

monkey库通过修改内存地址的方式,替换目标函数的实际执行地址,实现(几乎)任意函数的mock。你可以指定目标函数,然后定义一个匿名函数替换掉它。替换的记录会存在一个全局表里,不需要的时候可以通过它重新恢复原来的目标函数。

先看一个官方示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"os"
"strings"

"github.com/bouk/monkey"
)

func main() {
monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
s := make([]interface{}, len(a))
for i, v := range a {
s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
}
return fmt.Fprintln(os.Stdout, s...)
})
fmt.Println("what the hell?") // what the *bleep*?
}

可以看出调用fmt.Println已经替换成我们patch的方法了。

有时候在我们不仅要mock函数,而且在patch方法里还需要调用原来的函数。这时候需要使用monkey库提供的 PatchGuard结构体。关键在于,调用原来的函数之前先调用一次Unpatch,恢复到mock之前的情况;然后在调用了原函数之后,调用一次Restore

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
package main

import (
"fmt"
"strings"

"github.com/bouk/monkey"
)

func main() {
var guard *monkey.PatchGuard
guard = monkey.Patch(fmt.Println, func(a ...interface{}) (int, error) {
s := make([]interface{}, len(a))
for i, v := range a {
s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
}

// 取消patch
guard.Unpatch()
defer guard.Restore()
// 使用默认的fmt.Println
return fmt.Println(s...)
})
fmt.Println("what the hell?") // what the *bleep*?
fmt.Println("what the hell?") // what the *bleep*?
}

我们可以用第三方库来替换标准库,且不大范围修改原来代码,比如用json-iterator替换encoding/json来提升json的解析性能。

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
package main

import (
"encoding/json"
"fmt"

"github.com/bouk/monkey"
jsoniter "github.com/json-iterator/go"
)

type user struct {
ID string `json:"id"`
Name string `json:"name"`
}

func main() {
monkey.Patch(json.Marshal, func(v interface{}) ([]byte, error) {
fmt.Println("use jsoniter marshal")
return jsoniter.Marshal(v)
})

monkey.Patch(json.Unmarshal, func(data []byte, v interface{}) error {
fmt.Println("use jsoniter unmarshal")
return jsoniter.Unmarshal(data, v)
})
u1 := &user{
ID: "1",
Name: "0x5010",
}

u2 := &user{}

v, err := json.Marshal(u1)
fmt.Println(string(v), err)

err = json.Unmarshal(v, u2)
fmt.Println(u2, err)
}

Output:

1
2
3
4
use jsoniter marshal
{"id":"1","name":"0x5010"} <nil>
use jsoniter unmarshal
&{1 0x5010} <nil>

注意Go中虽然没有inline关键字,但仍存在inline函数,一个函数是否是inline函数由编译器决定。inline函数的特点是简单短小,在源代码的层次看有函数的结构,而在编译后却不具备函数的性质。inline函数不是在调用时发生控制转移,而是在编译时将函数体嵌入到每一个调用处,所以inline函数在调用时没有地址。通过命令行参数-gcflags=-l禁止inline,避免结果不符合预期。

如果想了解实现原理可以看作者的blog