虽然对于动态类型的语言,比如python或者js里,装饰器模式已经很常用
go中的函数是一等公民,可以直接作为参数传入传出,装饰器是可行的
但go的问题是静态类型,func addAllInt(a …int) 和 func addAllStr(a …string)是不同的类型。本身作为装饰器函数,只能传入出入参数一致的同类型的函数,这样限制了装饰器的应用场景。为了兼容通用的类型,需要使用反射来做一些事,但真正实现起来,却踩了不少坑,蛮有难度
需求:
go的协程一旦发生panic,就只能在自己的协程里捕获,一旦某个协程忘记了捕获,就会导致整个服务挂掉。因而就想实现一个通用的装饰器来捕获,给所有被装饰的函数添加,就无需在函数体内处理recover了。
效果类似于如下的python代码
import traceback
def trier(func):
def inner(*args, **kwargs):
try:
return func(*args, **kwargs)
except:
print(traceback.format_exc())
return inner
对于go来讲,困难点在于参数的静态类型,不支持*args这样的通用参数
因为这样的函数不会修改被装饰参数的出入参,考虑传入函数指针,反射利用指针替换具体的函数代码来实现功能
核心要点:
使用reflect.Makefunc来创建函数,注意使用[]reflect.Value来保存出入参数列表
使用reflect.Indirect(reflect.ValueOf(decoPtr)).Interface()拿到被装饰函数指针的接口对象oldFuncInterface,再通过out = reflect.ValueOf(oldFuncInterface).Call(in)来调用原函数,拿到响应结果
在调用原函数前,使用defer recover捕获异常,使用debug.Stack()打出堆栈
另外,因为使用了反射,堆栈会有反射库调用相关的很多信息,而这些信息是使用者不关心的,因而我以最内层的一次reflect.Makefunc调用为分界,处理掉了不必要的堆栈
下面是实现代码。可以说由于是静态类型语言,用这种通用装饰函数的代价不轻,所以不建议对非必要的函数使用通用类型的装饰器
func Catcher(decoPtr interface{}) {
var decoratedFunc, targetFunc reflect.Value
decoratedFunc = reflect.ValueOf(decoPtr).Elem()
targetFuncAll := reflect.Indirect(reflect.ValueOf(decoPtr)).Interface()
targetFuncType := reflect.TypeOf(targetFuncAll)
targetFunc = reflect.ValueOf(targetFuncAll) // valueOf and interface make a copy?
v := reflect.MakeFunc(targetFuncType,
func(in []reflect.Value) (out []reflect.Value) {
defer func() {
if err := recover(); err != nil {
fmt.Println(string(time.Now().AppendFormat(nil, "2006-01-02 15:04:05")),
"PANIC caught panic:", err)
trace := string(debug.Stack())
trace = "reflect.makeFunc" + strings.Join(strings.Split(trace, "reflect.makeFunc")[1:], "reflect.makeFunc")
fmt.Println("StackTrace:", trace)
out = make([]reflect.Value, targetFuncType.NumOut()) // todo make default value
for i, _ := range out {
out[i] = reflect.Zero(targetFuncType.Out(i))
}
}
}()
out = targetFunc.Call(in)
return
})
decoratedFunc.Set(v)
}
使用方式如下
func schedulerCenter(ctx context.Context) {
for {
b := <-taskChannel
job := b.asyncBuildHandler
Catcher(&job)
go job(ctx)
}
}
拿到函数变量(如果是静态定义的函数,需要先赋值给一个变量),然后用Catcher传入函数变量的指针,然后再去异步调用即可。同样适用于结构体和接口的方法(如上例子)