[转]go语言函数装饰器,接口类型变量反射赋值
转:原文:https://juejin.cn/post/7115343063119036453
------------------------
函数装饰
做基础组件经常需要用到函数修饰,例如我需要对所有被装饰方法里打印start、end。
已知函数签名的装饰
我们经常用的函数装饰器一般都是知道被装饰的方法的签名,然后返回一个同签名的方法。最简单的例子,kite的中间件,实际上就是对handler方法进行装饰。
type EndPoint func(ctx context.Context, req interface{}) (resp interface{}, err error)
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…
func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value
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方法
func TypeOf(i interface{}) Type
要装饰通用函数,所有装饰器的入参必须是interface 那么装饰器的初步设计就是
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的方法签名已经是固定的
func(args []Value) (results []Value)
所以直接考虑logicFunc的实现, 反射包里有个
func (v Value) Call(in []Value) []Value
可以实现任意函数反射后的调用,所以logicFunc内调用原函数也可以直接使用,类比middleware里的next(ctx, req)。
func(in []reflect.Value) []reflect.Value {
fn := reflect.ValueOf(f)
// ...
ret := fn.Call(in)
// ...
return ret
}
所以最终通用的函数装饰器为
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()
}
这个装饰器使用起来还是很别扭的
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,那么考虑出参也在使用前定义好签名。也就是装饰器把返回值当入参由使用方来传入。这也是左耳朵耗子推荐的用法
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
}
使用示例
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接口说明为
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.
那么对普通变量进行赋值
var a int64
v := reflect.ValueOf(a)
v.Set(reflect.ValueOf(int64(3)))
println(a)
这段代码是会panic的,可以看set接口的的说明,查看canset为false
println(v.CanSet())
因为go都是值复制的,所以v内的a只是个副本,不能set就好理解了。稍微变通一下就可以了,利用地址传递。 直接来个普通变量正常赋值的情况
var a int64
v := reflect.ValueOf(&a).Elem()
println(v.CanSet())
v.Set(reflect.ValueOf(int64(3)))
println(a)
对接口类型反射赋值
我们先定义一个接口和实现类
type itf interface {
String() string
}
type impl struct {
}
func (*impl) String() string {
return "itf impl"
}
参考普通变量赋值,可以写接口的赋值方法
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
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值构造器的
func Zero(typ Type) Value
Zero方法的入参typ 应该是我们需要操作的接口类型itf, 很自然会想到
var b itf
zero := reflect.Zero(reflect.TypeOf(b))
v.Set(zero)
但是这里的b实际上也是nil,所以reflect.Typeof(nil)并不会返回itf的type。所以需要转一道, 取b的地址,然后利用elem取值
var b itf
zero := reflect.Zero(reflect.TypeOf(&b).Elem())
v.Set(zero)
所以把接口类型变量置nil的完整写法应该是
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实际上是多余的,只是为了一个类型,可以省略掉
zero := reflect.Zero(reflect.TypeOf((*itf)(nil)).Elem())
综上完成了对接口类型置nil的操作。
链接:https://juejin.cn/post/7115343063119036453
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
2020-10-13 [转]Using files from web applications
2017-10-13 【转】gcov lcov 覆盖c/c++项目入门
2017-10-13 javascript 事件对象(event 对象)
2017-10-13 【转】keyCode对照表及JS监听组合按键
2017-10-13 再次讨论javascript 中的this
2016-10-13 写自己的一个pdo数据库操作框架
2016-10-13 javascript 基础系列(二)