张子阳的博客

首页 读书 技术 店铺 关于
张子阳的博客 首页 读书 技术 店铺 关于

Go反射动态调用方法

2018-11-21 作者: 张子阳 分类: Go 语言

反射是很多语言都提供的一种能力,它可以针对类型的元信息进行编程。例如获取类型的方法、字段、方法参数、方法返回值的信息。反射对于静态语言尤为重要,因为有了反射,可以使得静态语言变得"动态"一点。Go语言也提供了反射的能力,具体可以参考官方文章:The Laws of Reflection,以及reflect包的说明:Package reflect

这篇文章将实现一个常见的功能,即动态调用自定义struct的方法。

创建Computer结构

Computer结构包含了3个方法,我们最终的代码,要能够"动态"地调用这三个方法(以传递字符串的形式)。这三个方法的主要区别是参数和返回值的不同:

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使用反射动态创建对象 中解决。

感谢阅读,希望这篇文章能给你带来帮助!