Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically call method on interface{} regardless of receiver type

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.

like image 969
Nucleon Avatar asked Jan 02 '13 04:01

Nucleon


People also ask

What is interface {} Golang?

interface{} means you can put value of any type, including your own custom type. All types in Go satisfy an empty interface ( interface{} is an empty interface). In your example, Msg field can have value of any type.

Can we call method in interface?

In order to call an interface method from a java program, the program must instantiate the interface implementation program. A method can then be called using the implementation object.

CAN interface have variables in Golang?

Since the interface is a type just like a struct, we can create a variable of its type. In the above case, we can create a variable s of type interface Shape .

How do you implement an interface in Golang?

Implementing an interface in Go To implement an interface, you just need to implement all the methods declared in the interface. Unlike other languages like Java, you don't need to explicitly specify that a type implements an interface using something like an implements keyword.


2 Answers

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"))
}
like image 172
Nucleon Avatar answered Oct 14 '22 00:10

Nucleon


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

like image 45
Jeremy Wall Avatar answered Oct 14 '22 00:10

Jeremy Wall