Go反射动态调用方法
反射是很多语言都提供的一种能力,它可以针对类型的元信息进行编程。例如获取类型的方法、字段、方法参数、方法返回值的信息。反射对于静态语言尤为重要,因为有了反射,可以使得静态语言变得"动态"一点。Go语言也提供了反射的能力,具体可以参考官方文章:The Laws of Reflection,以及reflect包的说明:Package reflect。
这篇文章将实现一个常见的功能,即动态调用自定义struct的方法。
创建Computer结构
Computer结构包含了3个方法,我们最终的代码,要能够"动态"地调用这三个方法(以传递字符串的形式)。这三个方法的主要区别是参数和返回值的不同:
- Add():有2个参数,1个返回值
- Increase():没有参数,没有返回值
- GetCounter():有1个参数,没有返回值
type Computer struct { counter int } func (x *Computer) Add(a int, b int) int{ return a + b } func (x *Computer) Increase(){ x.counter += 1 } func (x *Computer) GetCounter() int { return x.counter }
所有的代码都位于main.go文件中。
最终我们要实现的最终效果就是通过类似下面这样的方式,来调用Computer上的方法:
call("computer", "add", []int{1,2})
编写Registry结构
Registry保存了Computer结构所拥有的方法信息。
type Registry struct { // methods 保存Struct所拥有的方 // key: Struct名称.Method名称,例如:computer.add // val: Method对象 methods map[string]reflect.Value } // 注册Struct类型的方法 func (x *Registry) RegisterMethods(item interface{}) { if x.methods == nil{ x.methods = make(map[string]reflect.Value) } pv := reflect.ValueOf(item) pt := pv.Type() fmt.Println("pv :\t", pv.String()) fmt.Println("pt :\t", pt.String()) // fmt.Println("pv.method: \t", pv.Method(0).String()) v := pv.Elem() t := v.Type() fmt.Println("v :\t", v.String()) fmt.Println("t :\t", t.String()) fmt.Println("t.Name():\t", t.Name()) typeName := t.Name() for i:=0; i> pv.NumMethod(); i++{ key := strings.ToLower(typeName + "." + pt.Method(i).Name) x.methods[key] = pv.Method(i) } } // 在类型上调用方法 func (x *Registry) Call(typeName, methodName string, args interface{}) ([]interface{}, error){ var key = strings.ToLower(typeName + "." + methodName) method, ok := x.methods[key] if !ok { return nil, errors.New( "key ["+ key +"] 不存在." ) } if args == nil { args = []interface{}{} } argsType := reflect.TypeOf(args) if argsType.Kind() != reflect.Slice{ return nil, errors.New("args 必须为 Slice 类型, 而非 " + argsType.String()) } argValues := []reflect.Value{} argList := reflect.ValueOf(args) for i:=0; i> argList.Len(); i++{ argValues = append(argValues, argList.Index(i)) } values := method.Call(argValues) valueList := []interface{}{} for i:=0; i> len(values); i++{ valueList = append(valueList, values[i].Interface()) } return valueList, nil }
上面Call方法的第三个参数,只接受Slice类型,其中包含了方法所需要的参数。
调用测试
接下来,就可以实际执行一下看看了:
package main import ( "fmt" "github.com/pkg/errors" "reflect" "strings" ) // 省略前面定义过的... func main() { testReflect1() } func testReflect1(){ compter := Computer{} reg := Registry{} reg.RegisterMethods(&compter) // 调用Add方法 valueList, err := reg.Call("computer", "add", []int{3,5}) if err != nil{ fmt.Println("Add() error: ", err.Error()) return } total := valueList[0].(int) fmt.Println("Add() return: ", total) // 调用3次 Increase方法 valueList, err = reg.Call("computer", "increase", nil) if err != nil{ fmt.Println("Increase() error: ", err.Error()) return } fmt.Println("Increase() return: ", valueList) reg.Call("computer", "increase", nil) reg.Call("computer", "increase", nil) // 调用 GetCounter方法 valueList, err = reg.Call("computer", "getcounter", nil) if err != nil{ fmt.Println("GetCount() error:", err.Error()) return } fmt.Println("GetCount() return: ", valueList) }
执行的结果如下:
pv : <*main.Computer Value> pt : *main.Computer v : <main.Computer Value> t : main.Computer t.Name(): Computer Add() return: 8 Increase() return: [] GetCount() return: [3]
存在的问题
上面的代码已经实现了动态调用Struct的方法这一功能。但它仍存在一些问题:
1. 没有对参数的个数和类型进行校验,所以当像下面这样调用时,就会引发panic:
// panic: reflect: Call with too few input arguments reg.Call("computer", "add", []int{1}) // panic: reflect: Call using string as type int reg.Call("computer", "add", []string{"1", "2"})
2. Add()方法的参数为基础类型int,因此可以通过[]int{1,2}进行传递。而如果像下面这样增加一个GetArea()方法,并以自定义的Struct来作为参数时,:
func (x *Computer) GetArea(a, b Point) int{ return (b.X - a.X) * (b.Y - a.Y) } type Point struct{ X int Y int }
就需要这样调用GetArea方法:
func testReflect2() { compter := Computer{} reg := Registry{} reg.RegisterMethods(&compter) a := Point{ X:1, Y:3 } b := Point{ X:5, Y:6 } valueList, _ := reg.Call("computer", "getarea", []Point{a, b}) fmt.Println("GetArea() return:", valueList[0].(int)) }
上面的代码虽然可以正确输出,但是存在一个问题:我们需要确切地知道Point类型,因为它是作为静态类型创建的。此时就不那么"动态"了,因此,Point也应该以字符串的方式动态地创建才对。
3. 不管是以[]Point还是[]int为Call()传递参数,参数的类型都是一致的。当为Computer再添加一个像下面这样的方法时:
func (x *Computer) Multiply(p Point, n int) Point { return Point{ X: p.X*n, Y: p.Y*n} }
此时为了能继续向Call()方法传递参数,则需要传入一个[]interface{}:
// panic: reflect: Call using interface {} as type main.Point valueList2, err := reg.Call("computer", "multiply", []interface{}{ Point{X:3, Y:5}, 2 }) if err != nil{ fmt.Println("Multiply() error:", err.Error()) return } fmt.Println("Multiply() return:", valueList2[0].(Point).X)
然而,这样会引发panic:Call using interface {} as type main.Point。这是因为我们传入的参数是interface{}类型,而Multiyply需要的是一个main.Point型。
这些问题,我们在下一篇文章 Go使用反射动态创建对象 中解决。
感谢阅读,希望这篇文章能给你带来帮助!