无论接收器类型如何,都在接口{}上动态调用方法

时间:2013-01-02 04:26:33

标签: go reflection go-reflect

我正在开发一个用Go编写的模板系统,这意味着它需要自由使用reflect包。在这种特定情况下,我需要能够在interface{}上动态调用方法。奇怪的是,只要我的数据属于已知类型,我的反射逻辑就可以正常工作,但如果数据类型为interface{}则不行。

以下示例您可以看到main()Pass()中的逻辑相同。唯一的区别是数据是interface{}

中的已知类型还是已知类型

去玩:http://play.golang.org/p/FTP3wgc0sZ

package main

import (
    "fmt"
    "reflect"
)

type Test struct {
    Start string
}

func (t *Test) Finish() string {
    return t.Start + "finish"
}

func Pass(i interface{}) {
    _, ok := reflect.TypeOf(&i).MethodByName("Finish")
    if ok {
        fmt.Println(reflect.ValueOf(&i).MethodByName("Finish").Call([]reflect.Value{})[0])
    } else {
        fmt.Println("Pass() fail")
    }
}

func main() {
    i := Test{Start: "start"}

    Pass(i)
    _, ok := reflect.TypeOf(&i).MethodByName("Finish")
    if ok {
        fmt.Println(reflect.ValueOf(&i).MethodByName("Finish").Call([]reflect.Value{})[0])
    } else {
        fmt.Println("main() fail")
    }
}

执行此代码后,我们得到以下结果

Pass() fail
startfinish

这意味着我的动态调用方法的方法工作正常,除非在我的对象当前位于interface{}的情况下。

如果我不使用指针接收器并传递i,那么它按预期工作。

去玩:http://play.golang.org/p/myM0UXVYzX

这让我相信我的问题是,当&iinterface{}时,我无法访问i(reflect.Value.Addr())的地址。我已经搜索了反射包并测试了诸如reflect.PtrTo()interface{}之类的东西,但我无法以我需要的方式工作。我的预感是它与{{1}}定义为参考对象这一事实有关。

2 个答案:

答案 0 :(得分:18)

感谢@Jeremy Wall我相信我能够解决我的问题。基本问题是在interface{}上调用动态命名的方法。有4个案例。

  1. interface{}基础数据是值,接收者是值
  2. interface{}基础数据是指针,接收者是值
  3. interface{}基础数据是值,接收者是指针
  4. interface{}底层数据是指针,接收器是指针
  5. 使用反射我们可以确定界面的下划线值。然后使用进一步反射,我们可以生成当前类型的备用数据类型。如果传入的数据是一个值,我们需要生成一个指向它的指针

    value := reflect.ValueOf(data)
    if value.Type().Kind() == reflect.Ptr {
        ptr = value
        value = ptr.Elem() // acquire value referenced by pointer
    } else {
        ptr = reflect.New(reflect.TypeOf(i)) // create new pointer
        temp := ptr.Elem() // create variable to value of pointer
        temp.Set(value) // set value of variable to our passed in value
    }
    

    现在我们有两种数据类型,我们可以简单地使用每种数据来检查现有方法

    var finalMethod reflect.Value
    method := value.MethodByName(methodName)
    if method.IsValid() {
        finalMethod = method
    }
    // check for method on pointer
    method = ptr.MethodByName(methodName)
    if method.IsValid() {
        finalMethod = method
    }
    
    if (finalMethod.IsValid()) {
        return finalMethod.Call([]reflect.Value{})[0].String()
    }
    

    因此,考虑到这一点,我们可以动态地调用任何方法,无论是声明为*receiver还是receiver

    完整的概念证明:http://play.golang.org/p/AU-Km5VjZs

    package main
    
    import (
        "fmt"
        "reflect"
    )
    
    type Test struct {
        Start string
    }
    
    // value receiver
    func (t Test) Finish() string {
        return t.Start + "finish"
    }
    
    // pointer receiver
    func (t *Test) Another() string {
        return t.Start + "another"
    }
    
    func CallMethod(i interface{}, methodName string) interface{} {
        var ptr reflect.Value
        var value reflect.Value
        var finalMethod reflect.Value
    
        value = reflect.ValueOf(i)
    
        // if we start with a pointer, we need to get value pointed to
        // if we start with a value, we need to get a pointer to that value
        if value.Type().Kind() == reflect.Ptr {
            ptr = value
            value = ptr.Elem()
        } else {
            ptr = reflect.New(reflect.TypeOf(i))
            temp := ptr.Elem()
            temp.Set(value)
        }
    
        // check for method on value
        method := value.MethodByName(methodName)
        if method.IsValid() {
            finalMethod = method
        }
        // check for method on pointer
        method = ptr.MethodByName(methodName)
        if method.IsValid() {
            finalMethod = method
        }
    
        if (finalMethod.IsValid()) {
            return finalMethod.Call([]reflect.Value{})[0].Interface()
        }
    
        // return or panic, method not found of either type
        return ""
    }
    
    func main() {
        i := Test{Start: "start"}
        j := Test{Start: "start2"}
    
        fmt.Println(CallMethod(i, "Finish"))
        fmt.Println(CallMethod(&i, "Finish"))
        fmt.Println(CallMethod(i, "Another"))
        fmt.Println(CallMethod(&i, "Another"))
        fmt.Println(CallMethod(j, "Finish"))
        fmt.Println(CallMethod(&j, "Finish"))
        fmt.Println(CallMethod(j, "Another"))
        fmt.Println(CallMethod(&j, "Another"))
    }
    

答案 1 :(得分:4)

在您的示例中,您不会使用支持Finish方法的内容调用pass,因为Finish仅在指向Test structs的指针上定义。在这种情况下,MethodByName正在做它应该做的事情。 *Test != Test他们完全是两种不同的类型。没有多少反射会将测试变成*测试。而且它也不应该。您可以使用PtrTo函数来确定是否在指针类型上定义了Finish方法,但这不会帮助您获得指向实际值的指针。

使用指针调用Pass:http://play.golang.org/p/fICI3cqT4t