Go实现AOP的几种想法
Go实现AOP
假设有store,从数据库获取数据,其中有方法IUserStore.GetByID,传入id参数,返回用户信息:
type IUserStore interface {
GetByID(ctx context.Context, id int) (User, error)
}
另外有service,刚好有用户id并且需要拿到用户信息,于是依赖了上述IUserStore:
type IUserSrv interface {
CheckUser(ctx context.Context, id int) error // 获取用户信息,然后检查用户某些属性
}
type userImpl struct {
userStore IUserStore
}
func (impl userImpl) CheckUser(ctx context.Context, id int) error {
user, err := impl.userStore.GetByID(ctx, id)
if err != nil {
return err
}
// 使用user数据做一些操作
_ = user
}
上面所描述的是一个最简单的情况,如果我们要在userImpl.CheckUser里对impl.userStore.GetByID方法调用添加耗时统计,依然十分简单。
func (impl userImpl) CheckUser(ctx context.Context, id int) error {
begin := time.Now()
user, err := impl.userStore.GetByID(ctx, id)
if err != nil {
return err
}
fmt.Println(time.Since(begin)) // 统计耗时
// 使用user数据做一些操作
_ = user
}
但是,如果方法里调用的类似impl.userStore.GetByID的方法非常之多,逻辑非常之复杂时,这样一个一个的添加,必然非常麻烦、非常累。
这时,如果有一个层间代理能帮我们拦截store的方法调用,在调用前后添加上耗时统计,势必能大大提升我们的工作效率。
比如:
func Around(f func(args []interface{}) []interface{}, args []interface{}) []interface{} {
begin := time.Now()
r := f(args)
fmt.Println(time.Since(begin)) // 统计耗时
return r
}
这只是一个简单的包装函数,怎么能将它与上面的接口联系到一起呢?
有兴趣的话,可以看这里的实现
可以看到,主要的方法是Around(provider interface{}, mock interface{}, arounder Arounder) interface{}
,
其中provider参数是类似NewXXX() IXXX
的函数,而mock是IXXX接口的一个实现,最后的Arounder是
拥有方法Around(pctx ProxyContext, method reflect.Value, args []reflect.Value) []reflect.Value
的接口。
这里的示例
可以看到,mock结构是长这样的:
type UserSrvMock struct {
CheckUserFunc func(ctx context.Context, id int) error
}
所以,为了提升开发效率,我还写了一个工具,用来根据接口生成相应的mock结构体。
代码生成替代反射
在上面描述的Around
实现里,依赖了reflect
包里的reflect.Value.Call
方法:
func (v Value) Call(in []Value) []Value
而这个方法的性能是比直接方法调用差的,因此,能不能用代码生成来替代它呢?
再回过头来看一下,我们通过provider
新建一个对象,这个对象带有我们需要使用的方法:
func NewIUserSrv(userStore IUserStore) IUserSrv {
return &userImpl{
userStore: userStore,
}
}
如果我们把provider
改为:
func NewIUserSrv(userStore IUserStore, withProxy bool) IUserSrv {
base := &userImpl{
userStore: userStore,
}
if withProxy { // 控制是否使用proxy
return getIUserSrvProxy(base)
}
return base
}
func getIUserSrvProxy(base IUserSrv) *UserSrvMock {
return &UserSrvMock{
CheckUserFunc: func(ctx context.Context, id int) error {
var r0 error
// 这里不就可以添加逻辑了吗
r0 = base.CheckUser(ctx, id)
// 这里不就可以添加逻辑了吗
return r0
},
}
}
这样,不就可以在调用该方法前后添加逻辑了吗?
如果接口的方法很多,并且添加的逻辑都一样,我们就需要考虑使用代码生成来提高开发效率了:
// 生成getIUserSrvProxy函数
func getIUserSrvProxy(base IUserSrv) *UserSrvMock {
return &UserSrvMock{
CheckUserFunc: func(ctx context.Context, id int) error {
// 通用逻辑:耗时统计
_gen_begin := time.Now()
var _gen_r0 error
_gen_ctx := UserSrvMockCheckUserProxyContext // 生成Mock时一并生成
_gen_cf, _gen_ok := _gen_customCtxMap[_gen_ctx.Uniq()] // _gen_customCtxMap:全局map,存储用户自定义proxy
if _gen_ok {
// 收集参数
_gen_params := []any{}
_gen_params = append(_gen_params, ctx)
_gen_params = append(_gen_params, id)
_gen_res := _gen_cf(_gen_ctx, base.CheckUser, _gen_params)
// 结果断言
_gen_tmpr0, _gen_exist := _gen_res[0].(error)
if _gen_exist {
_gen_r0 = _gen_tmpr0
}
} else {
// 原始调用
_gen_r0 = base.CheckUser(ctx, id)
}
log.Printf("[ctx: %s]used time: %v\n", _gen_ctx.Uniq(), time.Since(_gen_begin))
return _gen_r0
},
}
}
var (
userSrvMockCommonProxyContext = inject.ProxyContext{
PkgPath: "接口所在包路径,如:github.com/donnol/tools/inject",
InterfaceName: "接口名,如:IUserSrv",
}
UserSrvMockCheckUserProxyContext = func() (pctx inject.ProxyContext) {
pctx = userSrvMockCommonProxyContext
pctx.MethodName = "CheckUser" // 方法名
return
}()
)
var (
_gen_customCtxMap = make(map[string]inject.CtxFunc)
)
// 通过调用这个方法注册自定义proxy函数
func RegisterProxyMethod(pctx inject.ProxyContext, cf inject.CtxFunc) {
_gen_customCtxMap[pctx.Uniq()] = cf
}
func main() {
RegisterProxyMethod(UserSrvMockCheckUserProxyContext, func(ctx ProxyContext, method any, args []any) (res []any) {
log.Printf("custom call")
// 从any断言回具体的函数、参数
f := method.(func(ctx context.Context, id int) error)
a0 := args[0].(context.Context)
a1 := args[1].(id)
// 调用
r1 := f(a0, a1)
res = append(res, r1)
return res
})
}
最后,一个既能添加通用逻辑,又能添加定制逻辑的proxy
就完成了。
对于任意函数调用通过替换ast节点来添加Proxy
normal.go
:
package proxy
import (
"log"
)
func A(ctx any, id int, args ...string) (string, error) {
log.Printf("arg, ctx: %v, id: %v, args: %+v\n", ctx, id, args)
return "A", nil
}
func C() {
args := []string{"a", "b", "c", "d"}
r1, err := A(1, 1, args...)
if err != nil {
log.Printf("err: %v\n", err)
return
}
log.Printf("r1: %v\n", r1)
}
在上述代码中,C
函数调用了A
函数,那么,现在我想在这个调用前后添加耗时统计,该怎么办呢?
// 添加耗时统计
func C() {
begin := time.Now()
args := []string{"a", "b", "c", "d"}
r1, err := A(1, 1, args...)
if err != nil {
log.Printf("err: %v\n", err)
return
}
log.Printf("r1: %v\n", r1)
log.Printf("used time: %v\n", time.Since(begin))
}
如果,我能生成一个AProxy
函数,里面包含有耗时统计等逻辑,再把C
对A
的调用改为对Aproxy
的调用,是不是就非常方便了呢!
# 安装工具
go install github.com/donnol/tools/cmd/tbc@master
# 执行命令,生成代码
tbc genproxy -p ./parser/testtype/proxy/ --func A
gen_proxy.go
:
package proxy
import (
"log"
"time"
)
// 生成A的Proxy
func AProxy(ctx any, id int, args ...string) (string, error) {
begin := time.Now()
var r0 string
var r1 error
r0, r1 = A(ctx, id, args...)
log.Printf("used time: %v\n", time.Since(begin))
return r0, r1
}
normal.go
:
package proxy
import (
"log"
)
func A(ctx any, id int, args ...string) (string, error) {
log.Printf("arg, ctx: %v, id: %v, args: %+v\n", ctx, id, args)
return "A", nil
}
func C() {
args := []string{"a", "b", "c", "d"}
// 此处对A的调用就被替换为对AProxy的调用了
r1, err := AProxy(1, 1, args...)
if err != nil {
log.Printf("err: %v\n", err)
return
}
log.Printf("r1: %v\n", r1)
}
转载自:https://juejin.cn/post/7099621236455505957