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

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

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


person Nucleon    schedule 02.01.2013    source источник


Ответы (3)


Благодаря @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() // 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"))
}
person Nucleon    schedule 04.01.2013
comment
Спасибо. Хотя мой вопрос был другим, ваш ответ помог мне определить, есть ли в структуре метод с IsValid(). - person Matt; 21.11.2013
comment
Я придумал это (play.golang.org/p/v7lC0swB3s), когда пытался ответить мой собственный вопрос (stackoverflow.com/questions/ 20167935/) - person Brenden; 24.11.2013

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

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

person Jeremy Wall    schedule 02.01.2013
comment
Ваш ответ очень помог найти решение проблемы. Ключ был No amount of reflection will turn a Test into a *Test, что заставило меня понять, что решение проблемы основывается на поиске способа сделать именно это. Я добавил свой собственный ответ, который, как мне кажется, доказывает, что можно создать *Test из Test, используя отражение. Это позволяет мне вызывать любой метод на любом interface{} динамически по имени, независимо от типа приемника. - person Nucleon; 04.01.2013
comment
Действительно, вы можете использовать API отражения для создания нового типа, который является указателем на значение, и это будет работать. Но в соответствии с философией, согласно которой go работает, вы больше не работаете над тем же типом, который вы передали. В любом случае рад, что вы смогли решить свою проблему. - person Jeremy Wall; 04.01.2013

Это хороший пример 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)
}

код из: https://groups.google.com/d/msg/golang-nuts/8lK0WsGqQ-0/HJgsYW_HCDoJ

person Gary Gauh    schedule 08.12.2016