张子阳的博客

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

Go反射指定执行方法的对象

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

在上一篇 Go反射动态创建对象 中,我们在末尾又提出了三个问题,其中最后一个是执行方法的对象,我们希望能够自行指定方法是在哪个对象上执行的。这篇文章将演示如何实现这一过程。因为这已经是同一个主题的第3篇文章了,因此我们快速地过一遍代码。

修改Registry结构

现在,我们不再注册结构类型的方法,而是直接注册类型。

type Registry struct {
    // key: Struct名称,例如:main.Computer
    types map[string]reflect.Type
}

// 注册类型
func (x *Registry) RegisterType(t reflect.Type) error {
    if x.types == nil {
        x.types = make(map[string]reflect.Type)
    }
    // fmt.Println("t: \t", t.String())
    // fmt.Println("t.Name(): \t", t.Name())

    if _, ok := x.types[t.String()]; ok {
        return errors.New("类型: " + t.String() + "已经注册过了")
    }

    x.types[strings.ToLower(t.String())] = t
    return nil
}

我们要修改Call()方法的参数,将第一个参数的类型改为interface{}类型。当传入的是string时,则创建和类型名为此字符串相同的对象;当传入的是其他值时,则认为要在该对象上调用方法。

传入的应为指针类型,否则pv.MethodByName会找不到方法,并且下面的 field.Set() 也会失败,具体原因参看:The Laws of Reflection 的第3条。
// 在类型上调用方法
func (x *Registry) Call(target interface{}, methodName string, args interface{}) ([]interface{}, error) {

    var pv reflect.Value

    // 1. 验证target是string还是对象
    switch target.(type) {
    // 根据string创建一个新的对象,并在该对象上调用method方法
    case string:
        typeName := strings.ToLower(target.(string))
        t, ok := x.types[typeName]
        if !ok {
            return nil, errors.New("类型 " + typeName + " 尚未注册")
        }
        pv = reflect.New(t)
    default:
        // 获取target的reflect.Value对象
        pv = reflect.ValueOf(target)
    }

    if pv.Kind() != reflect.Ptr {
        return nil, errors.New("target 应为指针,但实际为: " + pv.Type().String())
    }

    fmt.Println("pv.Kind:", pv.Kind())
    fmt.Println("pv:\t", pv.String())

    // 2. 获取调用对象上的method对象
    method := pv.MethodByName(methodName)
    if !method.IsValid() {
        return nil, errors.New("方法 " + methodName + " 不存在.")
    }

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

    argsType := reflect.TypeOf(args)

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

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

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

    // 4.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{}", methodName))
        }

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

        if argType != mapType && inType != argType {
            return nil, errors.New(fmt.Sprintf("%s 的第%d个参数应为%s ,实际为%s ", methodName, 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
}

// 创建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()
}

func main() {
    testReflect2()
}

func testReflect2() {
    reg := Registry{}
    t := reflect.TypeOf(Computer{})
    reg.RegisterType(t)

    x := Computer{}

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

    reg.Call(x, "Increase", nil)
    reg.Call(x, "Increase", nil)

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

测试1:每次都在新对象上调用方法

func testReflect1() {
    reg := Registry{}
    t := reflect.TypeOf(Computer{})
    reg.RegisterType(t)

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

    reg.Call("main.Computer", "Increase", nil)
    reg.Call("main.Computer", "Increase", nil)

    values, err := reg.Call("main.Computer", "GetCounter", nil)
    if err != nil {
        fmt.Println("GetCount() error:", err.Error())
    } else if values != nil {
        fmt.Println("GetCount() return: ", values[0])
    }
}

这里GetCount()返回0,因为每次调用Increase()和GetCount()都在是一个新创建的对象上面。

这里对Registry中字典的key也做了优化,存入了package名称。因此调用时要加上main.(前面两篇文章没有加)

测试2:每次都在同一个对象上调用方法

func testReflect2() {
    reg := Registry{}
    t := reflect.TypeOf(Computer{})
    reg.RegisterType(t)

    x := &Computer{}

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

    reg.Call(x, "Increase", nil)
    reg.Call(x, "Increase", nil)

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

在上面的代码中,因为方法的调用都在同一个x对象上面,所以最后GetCount()时返回了3。通过这样的修改,我们可以根据需要决定在调用方法时是创建新对象,还是在现有对象上调用。

至此,我们就完成了GO反射的常见操作。但仍有诸多的不足,比如说:没有处理variadic function,即参数的个数为可变的方法。例如:func Add(all ...int)。上面的方法会报错:需要X个参数,但是传入了Y个参数。此时需要根据 methodType.IsVariadic() 方法去进行判断和处理。

对于这些问题,基于已经掌握的知识,都可以进行解决,就不再示范了。通过这三篇文章的练习,已经可以应付大多数用到反射的场景。

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