一般来说,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 接管了,这个对象就无法回收了。
而且绝大多数情况,我觉得业务代码是不需要动态代理能力的。
但是,优点也有:不需要代码生成。
如果你需要魔法,它肯定是好用的魔障 ,会让你成为最亮的“巴屙屙小魔仙”。