[转]go语言函数装饰器,接口类型变量反射赋值

发布时间 2023-10-13 18:06:22作者: 立志做一个好的程序员

 

转:原文:https://juejin.cn/post/7115343063119036453

------------------------

 

函数装饰

做基础组件经常需要用到函数修饰,例如我需要对所有被装饰方法里打印start、end。

已知函数签名的装饰

我们经常用的函数装饰器一般都是知道被装饰的方法的签名,然后返回一个同签名的方法。最简单的例子,kite的中间件,实际上就是对handler方法进行装饰。

 
go
复制代码
type EndPoint func(ctx context.Context, req interface{}) (resp interface{}, err error)
 
go
复制代码
func Middleware(next endpoint.EndPoint) endpoint.EndPoint {
   return func(ctx context.Context, req interface{}) (resp interface{}, err error) {
      // before do something...
      next(ctx, req)
      // after do something...
   }
}

所以本质上就是 fa(fb(fc(fd(req)))) 的调用形式。

这种函数装饰的基础在于每个装饰方法都是知道被装饰函数的签名的,当func签名改变后,装饰方法则不可用了。

如何解决通用函数的装饰?

装饰器本质上是要生成一个func,而反射包里有一个通用的生成func的方法,golang.org/pkg/reflect…

 
go
复制代码
func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value
 
sql
复制代码
MakeFunc returns a new function of the given Type that wraps the function fn. When called, that new function does the following:

- converts its arguments to a slice of Values.
- runs results := fn(args).
- returns the results as a slice of Values, one per formal result.

makefunc的入参是type、func 所以需要解决装饰方法的type,可以利用反射包里的TypeOf方法

 
go
复制代码
func TypeOf(i interface{}) Type

要装饰通用函数,所有装饰器的入参必须是interface 那么装饰器的初步设计就是

 
go
复制代码
func Decorate(f interface{}) interface{} {
   fn := reflect.ValueOf(f)
   v := reflect.MakeFunc(fn.Type(), logicFunc)
   return v.Interface()
}

下一步就是要解决logicFunc的问题了,根据MakeFunc的说明,v就是一个更高了type的func,v(args)的结果就是logicFunc(args)的执行结果 所以logicFunc的逻辑就是装饰器需要做的事,logicFunc的方法签名已经是固定的

 
scss
复制代码
func(args []Value) (results []Value)

所以直接考虑logicFunc的实现, 反射包里有个

 
scss
复制代码
func (v Value) Call(in []Value) []Value

可以实现任意函数反射后的调用,所以logicFunc内调用原函数也可以直接使用,类比middleware里的next(ctx, req)。

 
go
复制代码
func(in []reflect.Value) []reflect.Value { 
   fn := reflect.ValueOf(f)
   // ...
   ret := fn.Call(in)
   // ...
   return ret
}

所以最终通用的函数装饰器为

 
go
复制代码
func Decorate(f interface{}) interface{} {
   fn := reflect.ValueOf(f)
   logicFunc := func(in []reflect.Value) []reflect.Value { 
   	fn := reflect.ValueOf(f)
   	// before do something...
   	ret := fn.Call(in)
   	// after do something...
   	return ret
	}
   v := reflect.MakeFunc(fn.Type(), logicFunc)
   return v.Interface()
}

这个装饰器使用起来还是很别扭的

 
go
复制代码
func foo(a, b, c int) (int, error) {
   return a + b + c, nil
}

func main() {
   decorateFoo := Decorate(foo2)
   if fn, ok := decorateFoo.(func(a, b, c int) (int, error)); ok {
      ret, err := fn(1, 2, 3)
      fmt.Println(ret, err)
   }
}

每次用之前需要断言成被装饰的方法签名。

能不能不进行断言呢,因为被修饰的方法本身就是入参,但是为了通用性,入参必须定义为interface,那么考虑出参也在使用前定义好签名。也就是装饰器把返回值当入参由使用方来传入。这也是左耳朵耗子推荐的用法

 
go
复制代码
func Decorate(decoPtr, f interface{}) error {
   fn := reflect.ValueOf(f)
   decoratedFunc := reflect.ValueOf(decoPtr).Elem()
   logicFunc := func(in []reflect.Value) []reflect.Value { 
   	// before do something...
   	ret := fn.Call(in)
   	// after do something...
   	return ret
	}
   v := reflect.MakeFunc(fn.Type(), logicFunc)
   decoratedFunc.Set(v)
   return nil
}

使用示例

 
go
复制代码
func foo1(a, b, c int) (int, error) {
   return a + b + c, nil
}

func main() {
   decorateFoo := foo1
   Decorate(&decorateFoo, foo1)
   ret, err := decorateFoo(1, 2, 3)
   fmt.Println(ret, err)
}

反射更改接口类型的值

有这个想法是因为在通用函数的装饰里,假设所有方法的返回值最后一位都是error,但每个方法签名不一样,怎么能做到在通用装饰里,把所有的返回error置为nil

先看普通变量赋值场景

reflect包里有set**接口,其中通用set接口说明为

 
vbnet
复制代码
func (v Value) Set(x Value)

Set assigns x to the value v. It panics if CanSet returns false. As in Go, x's value must be assignable to v's type.

那么对普通变量进行赋值

 
css
复制代码
var a int64
v := reflect.ValueOf(a)
v.Set(reflect.ValueOf(int64(3)))
println(a)

这段代码是会panic的,可以看set接口的的说明,查看canset为false

 
scss
复制代码
println(v.CanSet())

因为go都是值复制的,所以v内的a只是个副本,不能set就好理解了。稍微变通一下就可以了,利用地址传递。 直接来个普通变量正常赋值的情况

 
css
复制代码
var a int64
v := reflect.ValueOf(&a).Elem()
println(v.CanSet())
v.Set(reflect.ValueOf(int64(3)))
println(a)

对接口类型反射赋值

我们先定义一个接口和实现类

 
go
复制代码
type itf interface {
   String() string
}

type impl struct {
}

func (*impl) String() string {
   return "itf impl"
}

参考普通变量赋值,可以写接口的赋值方法

 
css
复制代码
func main() {
   var a itf
   v := reflect.ValueOf(&a).Elem()
   println(v.CanSet())

   actual := &impl{}
   v.Set(reflect.ValueOf(actual))
   println(a.String())
}

对接口类型反射置nil

有的场景下,是接口类型有值,但是要把这个值置空,例如感知到下游的err后要返回nil,但是每个方法的签名不一样,只知道返回值最后一位是err

 
css
复制代码
var a itf
a = &impl{}
v := reflect.ValueOf(&a).Elem()
println(v.CanSet())

v.Set(reflect.ValueOf(nil))
println(a)

上述代码会直接保存,因为对nil进行反射取value得到的是空结果,set不进去。 分析我们需要set的其实是个接口itf的0值,reflect包里是有0值构造器的

 
go
复制代码
func Zero(typ Type) Value

Zero方法的入参typ 应该是我们需要操作的接口类型itf, 很自然会想到

 
css
复制代码
var b itf
zero := reflect.Zero(reflect.TypeOf(b))
v.Set(zero)

但是这里的b实际上也是nil,所以reflect.Typeof(nil)并不会返回itf的type。所以需要转一道, 取b的地址,然后利用elem取值

 
css
复制代码
var b itf
zero := reflect.Zero(reflect.TypeOf(&b).Elem())
v.Set(zero)

所以把接口类型变量置nil的完整写法应该是

 
css
复制代码
func main() {
   var a itf
   a = &impl{}
   v := reflect.ValueOf(&a).Elem()
   println(v.CanSet())

   var b itf
   zero := reflect.Zero(reflect.TypeOf(&b).Elem())
   v.Set(zero)
   println(a)
   println(a == nil)
}

分析发现 var b itf实际上是多余的,只是为了一个类型,可以省略掉

 
css
复制代码
zero := reflect.Zero(reflect.TypeOf((*itf)(nil)).Elem())

综上完成了对接口类型置nil的操作。


作者:会飞的星星
链接:https://juejin.cn/post/7115343063119036453
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。