虽然对于动态类型的语言,比如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传入函数变量的指针,然后再去异步调用即可。同样适用于结构体和接口的方法(如上例子)