Go 实现 AOP 切面操作,说到这个,有人可能会想,AOP 不是 Java 的玩意儿吗,怎么也拿到 Go 里来了?其实,编程思想嘛,并没有语言的边界。AOP(Aspect Oriented Programming)其实就是切面编程,简单理解,就是在程序正常运行流程之外,你找到几个点,切进去,插一脚。这么做,通常能干点日志记录,性能监控什么的。
AOP 在 Go 里怎么玩?
Go 并不是天然像 Java 那样有完善的 AOP 机制。在 Go 里,最直接的办法就是利用函数回调,用一点小小的编程技巧,创造自己的切面逻辑。
举个例子,我们想在一个函数前后打印日志,比如说有这样的加法函数:
go
func add(a, b int) int {
return a + b
}
直接调用,只得到个结果,干了什么不知道。现在想让每次运算前后都打一行日志,怎么做?直接改函数体当然可以,但这就违反了“分离关注点”的原则。理想的 AOP,就是让“加法”和“日志”井水不犯河水。
第一种方案:直接回调
在 Go 里,我们把原函数封装到一个回调里,再加个外围函数控制切面逻辑:
go
func logAdd(f func(int,int)int)func(int,int)int{
returnfunc(a, b int)int{
deferfunc(){ fmt.Println("after add")}()
fmt.Println("before add")
returnf(a, b)
}
}
这个logAdd
相当于我们切面逻辑的载体。每次调加法,先打一行“before add”,再执行函数f
,最后打“after add”。使用时变成:
go
func main() {
addWithLog := logAdd(add)
result := addWithLog(2, 3)
fmt.Println("result is", result)
}
这样做就是很基础的 AOP,但是局限性也明显:只能用作特定函数签名(参数和返回值类型固定)。处理其他场景不灵活。再来看更普适的做法。
第二种方案:利用反射
写框架或是通用工具时,不同函数有不同参数和返回类型,思路是利用 Go 的反射机制进行更宽泛的包装:
go
func Aspect(fn interface{}, aspectFns ...interface{})[]reflect.Value {
fnType := reflect.TypeOf(fn)
fnValue := reflect.ValueOf(fn)
aspects :=make([]reflect.Value,len(aspectFns))
for i, aspectFn :=range aspectFns {
aspects[i]= reflect.ValueOf(aspectFn)
}
returnfunc(in []reflect.Value)[]reflect.Value {
for_, aspect :=range aspects {
aspect.Call(in)
}
return fnValue.Call(in)
}
}
这个 Aspect 函数通过反射把原始函数和切面函数包装在了一起。不管你函数参数、返回值怎样,我把“进”和“出”的位置让切面逻辑插进去执行。利用就像这样:
go
func before(in []reflect.Value){
fmt.Println("aspect before")
}
funcafter(in []reflect.Value){
fmt.Println("aspect after")
}
funcmain(){
aspectAdd :=Aspect(add, before, after)
result :=aspectAdd([]reflect.Value{reflect.ValueOf(2), reflect.ValueOf(3)})
fmt.Println("result is", result[0].Int())
}
你可以把自己要插入的逻辑写成 before
和 after
,包装不同函数都很方便。缺点嘛,反射引入了额外性能开销,函数调用链变得更加晦涩。这种方法相比第一种,对于多种类型的函数都适用,代价是牺牲了一点运行效率。
实际使用场景
以上方法好不好,最终还得结合实际场景看。框架里插日志、性能监控,类似手段很常见。做中间件的朋友可能就要琢磨,选哪种看场景需求:功能稳了再用上反射倒也不虚,普普通通的回调算是是强在简单直接。有些时候,真的不需要硬套切面,就像早市上挑鱼,怎得新鲜就怎么来。不论黑猫白猫,抓到耗子的都是好猫。
写这些例子就是简单开个头。在你今后程序里,如何更好地插入代码“侧面行动”,要多琢磨个性需求。不怕一步一步实验,错了也不要紧,总是得能琢磨出的。你会发现 AOP 在 Go 里就有它的适应方式,挑合适的用即可。