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
存在的问题
这个方案似乎已经解决了我们所遇到的问题,但是还存在几个更深层次问题:
- 如果参数Point结构的字段 X、Y 为另一个结构,即结构存在嵌套时,没有针对这种情况进行处理。
- 有时候,我们并不知道Add()方法接受的具体类型,而只有一个字符串值(比如通过URL参数来调用方法localhost:8001/computer/add?a=2&b=5),那么就需要将传入的字符串(“2”和“5”),转换为方法参数的类型int。
- 上面的方法执行都和最初调用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反射指定执行方法的对象 中进行了实现。
感谢阅读,希望这篇文章能给你带来帮助!