无论接收器类型如何,动态调用interface{}上的方法

huangapple go评论75阅读模式
英文:

Dynamically call method on interface{} regardless of receiver type

问题

我正在使用Go语言编写一个模板系统,这意味着它需要大量使用reflect包。在这种特定情况下,我需要能够动态调用interface{}上的方法。奇怪的是,我的反射逻辑只在数据是已知类型时才能正常工作,而对于类型为interface{}的数据则不行。

在下面的示例中,您可以看到main()Pass()中的逻辑是相同的。唯一的区别是数据是已知类型还是已知类型在interface{}中。

Go Play链接: 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,则结果符合预期。

Go Play链接: http://play.golang.org/p/myM0UXVYzX

这让我相信,我的问题在于当iinterface{}时无法访问其地址(&i)。我已经仔细查阅了reflect包,并测试了reflect.Value.Addr()reflect.PtrTo()等方法,但都无法按照我需要的方式工作。我猜测问题可能与interface{}本质上是引用对象有关。

英文:

I'm working on a templating system written in Go, which means it requires liberal use of the reflect package. In this specific circumstance I need to be able to dynamically call a method on an interface{}. The oddity is that my reflection logic works fine as long as my data is of a known type, but not if the data is of type interface{}.

The the following example you can see that the logic in main() and Pass() is identical. The only difference is whether the data is a known type or a known type inside an interface{}

Go Play: 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")
	}
}

Upon executing this code we get the following result

Pass() fail
startfinish

Which means that my methodology for dynamically calling a method works fine except in a scenario when my object is currently in an interface{}.

If instead I do not use a pointer receiver and pass i then it works as expected.

Go Play: http://play.golang.org/p/myM0UXVYzX

This leads me to believe that my problem is that I cannot access the address of i (&i) when it is an interface{}. I've scoured the reflect package and tested things such as reflect.Value.Addr() and reflect.PtrTo() but I could not get either to work the way I needed. My hunch is that it has something to do with the fact that an interface{} is by definition a reference object.

答案1

得分: 27

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

  1. interface{}底层数据是值,接收器是值
  2. interface{}底层数据是指针,接收器是值
  3. interface{}底层数据是值,接收器是指针
  4. interface{}底层数据是指针,接收器是指针

使用反射,我们可以确定接口的底层值。然后使用进一步的反射,我们可以生成当前类型的替代数据类型。如果传入的数据是值,我们需要生成一个指向它的指针。

value := reflect.ValueOf(data)
if value.Type().Kind() == reflect.Ptr {
	ptr = value
	value = ptr.Elem() // 获取指针引用的值
} else {
	ptr = reflect.New(reflect.TypeOf(i)) // 创建新的指针
	temp := ptr.Elem() // 创建变量以存储指针的值
	temp.Set(value) // 将变量的值设置为传入的值
}

现在我们有了两种数据类型,我们可以使用每种类型来检查是否存在方法。

var finalMethod reflect.Value
method := value.MethodByName(methodName)
if method.IsValid() {
	finalMethod = method
}
// 检查指针上的方法
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
}

// 值接收器
func (t Test) Finish() string {
	return t.Start + "finish"
}

// 指针接收器
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 value.Type().Kind() == reflect.Ptr {
		ptr = value
		value = ptr.Elem()
	} else {
		ptr = reflect.New(reflect.TypeOf(i))
		temp := ptr.Elem()
		temp.Set(value)
	}
	
	// 检查值上的方法
	method := value.MethodByName(methodName)
	if method.IsValid() {
		finalMethod = method
	}
	// 检查指针上的方法
	method = ptr.MethodByName(methodName)
	if method.IsValid() {
		finalMethod = method
	}
	
	if (finalMethod.IsValid()) {
		return finalMethod.Call([]reflect.Value{})[0].Interface()
	}
	
	// 返回或抛出异常,找不到任何类型的方法
	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"))
}
英文:

Thanks to @Jeremy Wall I believe I was able to solve my problem. The basic issue is calling a dynamically named method on an interface{}. There are 4 cases.

  1. interface{} underlying data is value and receiver is value
  2. interface{} underlying data is pointer and receiver is value
  3. interface{} underlying data is value and receiver is pointer
  4. interface{} underlying data is pointer and receiver is pointer

Using reflection we can determine the underling value of our interface. Then using further reflection we can generate the alternate data type to our current type. If the data passed in was a value we need to generate a pointer to it

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
}

Now that we have both data types we can simply use each to check for an existing method

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()
}

Therefore with this in mind we can effectively call any method, dynamically, whether declared as *receiver or receiver.

Full Proof of Concept: 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"))
}

答案2

得分: 5

在你的例子中,你没有使用支持Finish方法的东西来调用pass,因为Finish只在指向Test结构体的指针上定义。在这种情况下,MethodByName正好做了它应该做的事情。*Test != Test它们是完全不同的两种类型。无论如何反射都不能将Test转换为*Test。而且实际上也不应该这样做。你可以使用PtrTo函数来查找指针类型上是否定义了Finish方法,但这并不能帮助你获得指向实际值的指针。

使用指针调用Pass可以正常工作:http://play.golang.org/p/fICI3cqT4t

英文:

In your example you don't call pass with something that supports the Finish method since Finish is only defined on pointers to Test structs. MethodByName is doing exactly what it should in that case. *Test != Test they are two different types completely. No amount of reflection will turn a Test into a *Test. And really it shouldn't either. You can use the PtrTo function to find out if the Finish method is defined on the pointer type but that won't help you get a pointer to the actual value.

Calling Pass with a pointer works: http://play.golang.org/p/fICI3cqT4t

答案3

得分: 0

这是一个使用golang实现类似功能的好例子:

package main

import "fmt"

type Parent struct {
    Attr1 string
}

type Parenter interface {
    GetParent() Parent
}

type Child struct {
    Parent //嵌入
    Attr   string
}

func (c Child) GetParent() Parent {
    return c.Parent
}

func setf(p Parenter) {
    fmt.Println(p)
}

func main() {
    var ch Child
    ch.Attr = "1"
    ch.Attr1 = "2"

    setf(ch)
}

代码来源:https://groups.google.com/d/msg/golang-nuts/8lK0WsGqQ-0/HJgsYW_HCDoJ

英文:

This is a good example of golang to implement similar functionality:

package main

import "fmt"

type Parent struct {
    Attr1 string
}

type Parenter interface {
    GetParent() Parent
}

type Child struct {
    Parent //embed
    Attr   string
}

func (c Child) GetParent() Parent {
    return c.Parent
}

func setf(p Parenter) {
    fmt.Println(p)
}

func main() {
    var ch Child
    ch.Attr = "1"
    ch.Attr1 = "2"

    setf(ch)
}

code from : https://groups.google.com/d/msg/golang-nuts/8lK0WsGqQ-0/HJgsYW_HCDoJ

huangapple
  • 本文由 发表于 2013年1月2日 12:26:33
  • 转载请务必保留本文链接:https://go.coder-hub.com/14116840.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定