Динамический метод вызова на интерфейсе {} независимо от типа приемника

Я работаю над системой шаблонов, написанной на 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

Это заставляет меня думать, что моя проблема в том, что я не могу получить адрес я (&i), когда он является interface{}. Я просмотрел пакет отражения и протестировал такие вещи, как reflect.Value.Addr() и reflect.PtrTo(), но я не мог работать так, как мне было нужно. Моя догадка заключается в том, что она имеет какое-то отношение к тому, что interface{} по определению является ссылочным объектом.

Ответ 1

Благодаря @Jeremy Wall я считаю, что смог решить мою проблему. Основная проблема - вызов динамически названного метода на interface{}. Есть 4 случая.

  • interface{} базовые данные - это значение, а получатель - значение
  • interface{} базовыми данными являются указатель и получатель - значение
  • interface{} базовые данные - это значение, а получатель - указатель
  • interface{} базовые данные - указатель, а приемник - указатель

Используя отражение, мы можем определить значение подрыва нашего интерфейса. Затем, используя дополнительное отражение, мы можем сгенерировать альтернативный тип данных для нашего текущего типа. Если переданные данные были значением, нам нужно сгенерировать указатель на него

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

Ответ 2

В вашем примере вы не вызываете pass с чем-то, что поддерживает метод Finish, поскольку Finish определяется только указателями на Test structs. MethodByName делает именно то, что ему нужно в этом случае. *Test != Test они полностью разные. Никакое количество отражений не превратит тест в * Тест. И на самом деле это тоже не должно быть. Вы можете использовать функцию PtrTo, чтобы узнать, определен ли метод Finish для типа указателя, но это не поможет вам получить указатель на фактическое значение.

Calling Pass с указателем работает: http://play.golang.org/p/fICI3cqT4t

Ответ 3

Это хороший пример golang для реализации аналогичных функций:

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