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反射指定执行方法的对象 中进行了实现。
感谢阅读,希望这篇文章能给你带来帮助!