golang 下反射 plugin 中的类型实例实现动态注入

设想一个场景,我们需要和其他团队配合一起开发,并且不想使用源码构建,也就是说最终希望通过集成对方发布的二进制的方式来完成部署。

那么我们有两种方式可以解决这个问题:

  • 定义进程间api,各自构建进程,通过进程间调用
  • 定义接口,实现者编译成动态库,运行时通过某些机制加载动态库然后再想办法获取到对应的实现的实例

对于c语言来说其实编译成动态库的方式是很自然的,但是golang的话就需要利用plugin机制。

首先我们需要使用go build -buildmode=plugin来将一个包含main包的代码编译成动态链接库。

1
2
p, _ := plugin.Open("plugin_name.so") // open so
f, _ := p.Lookup("routerFactory") // 查找符号

注意这里的plugin.Open并不会执行main包,而是仅仅会去调用所有包init函数。

p.Lookup("routerFactory")可以查找到名字为routerFactory的全局变量,我们可以要求实现者以一个固定的全局变量来实现我们的接口,接下来我们只需要断言到自己的interface,就可以调用了。注意,只有导出的变量或者函数符号才能被查找到。

如果我们想更加灵活一些,我们希望能查找未导出的符号,或者通过包名+类型名反射出实例,比如我们需要使用方法而不是全局的函数,那咋办。

这时候我们就要掏出抄代码大法了,go的reflect包内里面其实有这段代码typesByString

我们可以利用go:linkname来链接到reflect包内的实现,我们只用申明签名即可。注意使用这个需要import unsafe

1
2
//go:linkname typesByString reflect.typesByString
func typesByString(s string) []*_type

为了完整的实现,我们还需要从源码里面拷贝一些结构体和方法出来,比如type的定义,pkgpath()方法等,不用完整拷贝,调用的到的片段弄出来就行。

最后把这些骚代码封装到我们的internal包内,

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
//go:linkname typesByString reflect.typesByString
func typesByString(s string) []*_type

func searchForType(pkgPath, typeName string) (*_type, error) {
tbs := typesByString("*" + typeName)
for _, tb := range tbs {
p := (*ptrtype)(unsafe.Pointer(tb))
path := p.elem.pkgpath()
if path == pkgPath {
return &p.typ, nil
}
}
return nil, fmt.Errorf("%s, %s not found in executable image", pkgPath, typeName)
}

func Instantiate(pkgPath, typeString string, isPtr bool) (reflect.Value, error) {
res, err := searchForType(pkgPath, typeString)
if err != nil {
return reflect.Value{}, err
}
var emptyFace interface{}
ptr := (*eface)(unsafe.Pointer(&emptyFace))
ptr._type = res
ty := reflect.ValueOf(emptyFace).Type().Elem()
if isPtr {
e := reflect.New(ty)
return e, nil
}
e := reflect.New(ty).Elem()
return e, nil
}

对外就暴露个api即可。

1
2
// Instantiate returns an instance of a given full-package-path type string.
func Instantiate(typ string) (interface{}, reflect.Value, error)

测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
"fmt"
"testing"

"github.com/titanxxh/di/example"
_ "github.com/titanxxh/di/example/en"
)
func TestInstantiate(t *testing.T) {
{
a, _, err := Instantiate("github.com/titanxxh/di/example/en.english")
if err != nil {
panic(err)
}
fmt.Println(a.(example.Greeter).Hello())
}
}

然而并不能正确反射出来,报github.com/titanxxh/di/example/en, en.english not found in executable image,这就很奇怪了,我明明匿名引入了啊。

最后经过一番调查才发现,go会优化掉没使用的类型,我们需要搞个全局变量引用到自己的类型一下才行。。。比如这样。

1
2
3
4
5
6
var avoidOpt interface{}

func init() {
// use this symbol to avoid compiler optimization
avoidOpt = english{}
}

于是我们终于正确获得输出hello

再结合之前的plugin,就是我们的example啦。

项目地址在此,求个star呗。