一般来说,Go 这种纯静态的编译型语言,想实现像 Spring 那样的动态代理基本上是不可能实现的。


"你想要啊?悟空,你要是想要的话你就说话嘛,你不说我怎么知道你想要呢,虽然你很有诚意地看着我,可是你还是要跟我说你想要的。你真的想要吗?那你就拿去吧!你不是真的想要吧?难道你真的想要吗?……"


但是,如果你真的想要,也不是一定不可以。

使用 DPIG

DPIG 是一个实验性质的动态代理库,它不依赖代码生成技术。

它可以对接口的实例进行动态增强,使用方法也很简单:GitHub - cocotyty/dpig: Dynamic Proxy Implementation In Go它可以对接口的实例进行动态增强,使用方法也很简单:

var u UserStore = user.New()
// 此处进行增强
dpig.Component(&u)

var postCall = func(in, out []reflect.Value) {
    log.Println("Get User:", in[0].Interface(),out[0].Interface{})
}
// 修改方法运行行为
dpig.Change(dpig.MethodSelector{Object:"UserStore",Method:"GetUser"}, dpig.Extend{Post: []dpig.PostCall{postCall}})

u.GetUser(uid) // 此时会执行 postCall 函数


它支持对方法进行三种增强:前置、后置、环绕。


举个例子

常见的 CRUD 项目里面,往往有类似这样的、用于获得用户信息的接口:

type User struct {
    ID   int
    Name string
    Age  int
}

type UserStore interface {
    GetUser(ctx context.Context, id int) (u *User, err error)
}

这里我们给出一个最简单的 UserStore 实现:

type MemoryUserStore struct {
	users []*User
}

func (m *MemoryUserStore) GetUser(ctx context.Context, id int) (u *User, err error) {
	for _, user := range m.users {
		if user.ID == id {
			return user, nil
		}
	}
	return nil, errors.New("user is not found")
}

运行一下这个程序:

func main() {
	ctx := context.Background()

	var store UserStore
	store = &MemoryUserStore{users: []*User{
		{1, "Tom", 12},
		{2, "Jim", 12},
		{4, "Sam", 12},
	}}
	user, err := store.GetUser(ctx, 1)
        log.Println(user,err)
}

输出:

 &{1 Tom 12} <nil>


动态增加日志

我们写一个简单的日志增强的函数,用于在方法调用后,打印这次调用的传入参数和返回值:

func methodLogger(in, out []reflect.Value) {
	buf := bytes.NewBuffer(nil)
	for i, value := range in {
		buf.WriteString(fmt.Sprint(value.Interface()))
		if i != len(in)-1 {
			buf.WriteString(",")
		}
	}
	inStr := buf.String()

	buf.Reset()
	for i, value := range out {
		buf.WriteString(fmt.Sprint(value.Interface()))
		if i != len(out)-1 {
			buf.WriteString(",")
		}
	}
	outStr := buf.String()
	log.Println("pass: [", inStr, "] return: [", outStr,"]")
}

稍微修改一下上面那个 main 函数,给 UserStore.GetUser 添加这个后置增强函数:

func main() {
	ctx := context.Background()

	var store UserStore
	store = &MemoryUserStore{users: []*User{
		{1, "Tom", 12},
		{2, "Jim", 12},
		{4, "Sam", 12},
	}}

	dpig.Component(&store)

	dpig.Change(dpig.MethodSelector{
		Object: "UserStore",
		Method: "GetUser",
	}, dpig.Extend{Post: []dpig.PostCall{methodLogger}})

	store.GetUser(ctx, 1)
}

运行上面这个函数:

 pass: [ context.Background,1 ] return: [ &{1 Tom 12},<nil> ]

动态断路

我们写个断路器增强,能在程序运行时断路这个 GetUser 方法。

断路器 Breaker:

type Breaker struct {
	b uint32
}

func (b *Breaker) Break() {
	atomic.StoreUint32(&b.b, 1)
}

func (b *Breaker) Restore() {
	atomic.StoreUint32(&b.b, 0)
}

如代码所示,Breaker 有个整数字段 b 指示是否开启断路,当 b 为 1 时,开启断路,反之,关闭断路。

接下来,我们实现一下 dpig.AroundCall 类型的函数,即环绕增强。

func (b *Breaker) Around(in []reflect.Value, next func([]reflect.Value) []reflect.Value) (out []reflect.Value) {
	if atomic.LoadUint32(&b.b) == 1 {
		var nilUser *User
		return []reflect.Value{
			reflect.ValueOf(&nilUser).Elem(), reflect.ValueOf(errors.New("blocked")),
		}
	}
	return next(in)
}


接下来,使用 dpig 增强一下,并运行验证。

func main() {
	ctx := context.Background()

	var store UserStore
	store = &MemoryUserStore{users: []*User{
		{1, "Tom", 12},
		{2, "Jim", 12},
		{4, "Sam", 12},
	}}
	user, err := store.GetUser(ctx, 1)
        // 此时应该输出  &{1 Tom 12} <nil> 
	log.Println(user, err)

	dpig.Component(&store)

	breaker := &Breaker{}

	dpig.Change(dpig.MethodSelector{
		Object: "UserStore",
		Method: "GetUser",
	}, dpig.Extend{Around: []dpig.AroundCall{breaker.Around}})

	user, err = store.GetUser(ctx, 1)

        // 此时还未开启断路,应该原样输出  &{1 Tom 12} <nil> 
	log.Println(user, err)

        // 开启断路
	breaker.Break()

        // 此时还开启断路,将输出 <nil> blocked
	user, err = store.GetUser(ctx, 1)
	log.Println(user, err)

        // 关闭断路
	breaker.Restore()

	user, err = store.GetUser(ctx, 1)
        // 将输出  &{1 Tom 12} <nil> 
	log.Println(user, err)
}


输出:

2021/07/22 18:23:20 &{1 Tom 12} <nil>
2021/07/22 18:23:20 &{1 Tom 12} <nil>
2021/07/22 18:23:20 <nil> blocked
2021/07/22 18:23:20 &{1 Tom 12} <nil>


总结

DPIG 的缺点,也很多、很明显。

第一,它只能对接口进行增强。

第二,Component 必须传入接口指针。

第三,一旦被 DPIG 接管了,这个对象就无法回收了。

而且绝大多数情况,我觉得业务代码是不需要动态代理能力的。

但是,优点也有:不需要代码生成。

如果你需要魔法,它肯定是好用的魔障 ,会让你成为最亮的“巴屙屙小魔仙”。