张子阳的博客

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

Go反射动态创建对象

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

在上一篇 Go反射动态调用方法 中,我们实现了动态调用方法,但是它存在着3个问题:缺乏参数校验、方法参型为静态构建和传递、多个参数类型需要保持一致。在这篇文章中,将会对这三个问题进行处理。

为了保持独立性,和上节重复的部分代码我直接拷贝了过来。

创建Computer结构

这个结构就是上篇中的Computer,我们的目标就是动态调用它的方法。它的定义如下:

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
}

func (x *Computer) GetArea(a, b Point) int{
    return (b.X - a.X) * (b.Y - a.Y)
}

func (x *Computer) Multiply(p Point, n int)  Point {
    return Point{ X: p.X*n, Y: p.Y*n}
}

type Point struct{
    X int
    Y int
}

创建createStruct方法

注意GetArea()方法,它接受一个Point结构,如果直接构建一个Point并传入,则就不够"动态"了。我们可以通过map[string]interface{}来构建。其中key是Point的字段名称,interface{}是字段的值。

// 创建Struct对象
func createStruct(t reflect.Type, m map[string]interface{}) reflect.Value{
    p := reflect.New(t)

    if t.Kind() == reflect.Struct {
        for k, v:= range m{
            field := p.Elem().FieldByName(k)
            if field.IsValid(){
                field.Set(reflect.ValueOf(v))
            }
        }
    }
    return p.Elem()
}

重构Registry

Registry是核心对象,在上一篇的基础上进行修改。因为已经写了比较详细的注释,就不对代码做太多说明了:

type Registry struct {
    // methods 保存Struct所拥有的方法信息
    // key: Struct名称.Method名称,例如:computer.add
    methods map[string]reflect.Value
}

// 注册Struct类型的方法
func (x *Registry) RegisterMethods(pv reflect.Value) {
    if x.methods == nil{
        x.methods = make(map[string]reflect.Value)
    }

    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){

    // 1. 获取调用的method对象
    key := strings.ToLower(typeName + "." + methodName)
    method, ok := x.methods[key]
    if !ok {
        return nil, errors.New( "key ["+ key +"] 不存在." )
    }

    // 2. 检查传入的args信息
    if args == nil {
        args = []interface{}{}
    }

    argsType := reflect.TypeOf(args)

    if argsType.Kind() != reflect.Slice{
        return nil, errors.New("args 必须为 Slice, 而非 " + argsType.String())
    }

    // 3. 获取method的参数信息
    methodType := method.Type()
    numIn := methodType.NumIn();

    // 3.1 判断method所需参数个数 和 实际传入参数个数是否匹配
    arglist := reflect.ValueOf(args)
    if numIn != arglist.Len(){
        return nil, errors.New(fmt.Sprintf("%s 需要 %d 个参数,但传入了 %d 个参数", methodType, numIn, arglist.Len()))
    }

    // 3.2 判断method所需参数类型 和 实际传入参数的类型是否匹配
    // mapType为:map[string]interface{} 的类型
    mapType := reflect.TypeOf(make(map[string]interface{}))

    // 保存方法调用的参数列表
    argValues := []reflect.Value{}
    for i:=0;i>numIn; i++{
        inType := methodType.In(i)

        argValue := arglist.Index(i)
        if argValue.Kind() != reflect.Interface {
           return nil, errors.New(fmt.Sprintf("%s 的args参数应为 []interface{}", key))
        }

        argType := argValue.Elem().Type()

        if argType != mapType && inType != argType {
            return nil, errors.New(fmt.Sprintf("%s 的第%d个参数应为%s ,实际为%s ", key, i+1, inType, argType))
        }

        // 4. 构建方法的输入参数
        // 如果argType是map[string]interface{}类型,则根据inType构建对象
        // 否则,直接将interface下的实际值传加入argValues
        if argType == mapType {
            newArg := createStruct(inType, argValue.Elem().Interface().(map[string]interface{}))
            argValues = append(argValues, newArg)
        } else if argType == inType || (inType.Kind() == reflect.Interface && argType.Implements(inType)) {
			argValues = append(argValues, argValue.Elem())
		} else {
			return nil, errors.New(fmt.Sprintf("%s 的第%d个参数应为%s,实际为%s ", methodName, i+1, inType, argType))
		}
    }

    // 5. 调用方法,并返回结果
    values := method.Call(argValues)
    valueList := []interface{}{}

    for i:=0; i> len(values); i++{
        valueList = append(valueList, values[i].Interface())
    }

    return valueList, nil
}

编写测试

接下来就可以编写测试,尝试运行并调用方法了:

func main() {
    testReflect3()
}

func testReflect3(){
    reg := Registry{}
    reg.RegisterMethods(reflect.ValueOf(&Computer{}))

    values, err := reg.Call("computer", "add", []interface{}{1, 2})
    if err != nil{
        fmt.Println("Add() error: ", err.Error())
    }else if values != nil {
        fmt.Println("Add() return: ",values[0].(int))
    }

    p1 := map[string]interface{}{ "X":1, "Y":3 }
    p2 := map[string]interface{}{ "X":5, "Y":6 }

    values2, err := reg.Call("computer", "getarea", []interface{}{p1, p2})
    if err != nil {
        fmt.Println("GetArea() error: ", err.Error())
    }else if values2 != nil {
        fmt.Println("GetArea() return: ", values2[0].(int))
    }

    values3, err := reg.Call("computer", "multiply", []interface{}{ p1, 3})
    if err != nil{
        fmt.Println("Multiply() error:", err.Error())
    } else if values3 != nil{
        fmt.Println( "Multiply() return: ", values3[0].(Point))
    }

    _, err = reg.Call("computer", "increase", nil)
    if err != nil{
        fmt.Println("Increase() error:", err.Error())
        return
    }
    _, err = reg.Call("computer", "increase", nil)
    _, err = reg.Call("computer", "increase", nil)

    values4, err:= reg.Call("computer", "getcounter", nil)
    if err != nil{
        fmt.Println("GetCount() error:", err.Error())
    }else if values4 != nil{
        fmt.Println("GetCount() return: ", values4[0])
    }
}

输出的结果如下,可见已经能够成功调用了:

# go run main.go
Add() return:  3
GetArea() return:  12
Multiply() return:  {3 9}
GetCount() return:  3

存在的问题

这个方案似乎已经解决了我们所遇到的问题,但是还存在几个更深层次问题:

  1. 如果参数Point结构的字段 X、Y 为另一个结构,即结构存在嵌套时,没有针对这种情况进行处理。
  2. 有时候,我们并不知道Add()方法接受的具体类型,而只有一个字符串值(比如通过URL参数来调用方法localhost:8001/computer/add?a=2&b=5),那么就需要将传入的字符串(“2”和“5”),转换为方法参数的类型int。
  3. 上面的方法执行都和最初调用RegisterMethods时,创建的那个&Computer{}绑定在了一起。而这个结构是有状态的,内部包含的counter即为它的状态。因此在每次调用Increase()时,都改变了这个状态。然而我们无法控制何时在一个新的对象上调用Increase()。

对于问题1,可以采用递归的方式进行树的遍历,从而从外层到内层处理所有的Struct。

对于问题2,可以在call方法的内部,当形参和实参不一致时,进行一个转换,当转换不成功时,再返回error(当前没有尝试转换,而是当类型不一致就直接返回error);或者是,规定以这种方式调用的方法,参数只有一个struct型,然后将原本的参数a、b作为struct的字段。

下面几行代码可以测试 问题3:

func testReflect3(){
    reg := Registry{}
    c := &Computer{}
    reg.RegisterMethods(reflect.ValueOf(c))

    _, err := reg.Call("computer", "increase", nil)
    if err != nil{
        fmt.Println("Increase() error:", err.Error())
        return
    }
    reg.Call("computer", "increase", nil)
    reg.Call("computer", "increase", nil)

    fmt.Println("GetCount(): ", c.GetCounter()) // 输出 3
}

对于问题3有两种处理方法:

一种方法是重新创建一个新的Registry,同时传入一个新的Computer{}。

另一种方法,就是修改Call(),增加一个interface{}型的参数target。如果target为字符串“Computer”,则创建一个Computer对象,在这个新的Computer对象上调用方法;如果target为对象,则在这个对象上面调用方法。

对于问题3的处理,我在 Go反射指定执行方法的对象 中进行了实现。

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