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使用反射动态创建对象 中解决。
感谢阅读,希望这篇文章能给你带来帮助!