3
votes

I have recently been writing some auditing processes in go. Most of this is just reflection ripping arbitrary constructs apart. There exists an interface that can be used for object equality. Not all of the things being ripped apart implement this interface and the comparisons can be between arbitrary types (object equality does not imply equality of type).

Since the things being compared do not need to be the same type there are cases where something is a pointer on one side and a value on the other (*string vs string or *ChickenBox vs BoxOfChicken). I have been simply dereferencing pointers/interface values when I encounter them so the comparison when done is simple.

My problem is that once I have dereferenced the pointer/interface I don't seem to be able to type assert to interfaces that the parent pointer/interface implemented.

Here is an example (playground: https://play.golang.org/p/O_dvyW07fu) that demonstrates this:

package main

import (
    "fmt"
    "reflect"
)

// ID interface
type IHasID interface {
    GetID() string
    SetID(id string)
}

// implementing type
type HasID struct {
    ID string `bson:"_id,omitempty"`
}

func (be *HasID) GetID() string {
    return be.ID
}
func (be *HasID) SetID(id string) {
    be.ID = id
}

// static comparison reflect.Type of IHasID
var idType = reflect.TypeOf((*IHasID)(nil)).Elem()

func main() {
    testy := struct {
        HasID
        Somefield int
    }{
        HasID{
            ID: "testymctest",
        },
        4,
    }

    id, _ := GetIDFrom(reflect.ValueOf(testy))
    fmt.Printf("ID: %v\n", id)
}

// check if the reflect.Value implements IHasID
func ImplementsID(x reflect.Value) bool {
    switch x.Kind() {
    case reflect.Interface, reflect.Ptr:
        if x.IsValid() && !x.IsNil() {
            return ImplementsID(x.Elem())
        }
    case reflect.Struct:
        if x.IsValid() {
            return reflect.PtrTo(x.Type()).Implements(idType)
        }
    }
    return false
}

// check and get the reflect.Value's ID
func GetIDFrom(x reflect.Value) (string, bool) {
    switch x.Kind() {
    case reflect.Ptr, reflect.Interface:
        if x.IsValid() && !x.IsNil() {
            return GetIDFrom(x.Elem())
        }
    case reflect.Struct:
        if x.IsValid() && ImplementsID(x) {
            // not using val,ok syntax to demo issue, if it gets here it should
            // implement ID since we did the check in the if statement above
            val := x.Interface().(IHasID)
            return val.GetID(), true
        }
    }
    return "", false
}

My understanding of this is that the interface is two parts (https://blog.golang.org/laws-of-reflection). Within reflect this is represented by reflect.Type (broadly methods) and reflect.Value (broadly data). Within the demonstration we evaluate successfully that the passed reflect.Value's reflect.Type implements the interface on line 67:

if x.IsValid() && ImplementsID(x) {

This is because on line 53 we can reconstruct our relationship to the interface using reflect.PtrTo:

return reflect.PtrTo(x.Type()).Implements(idType)

It is this process (on the reflect.Type side) that I am unable to replicate on the reflect.Value side. So on line 70 we simply have the underlying struct (without methods):

val := x.Interface().(IHasID)

This fails and I have been unable to find any syntax that will allow me to do what seems to have been possible on the reflect.Type side; get back to the methods defined against that struct that meet the assert's interface (I suppose it is plausible that the reflect.Type is simply working against a cached interface store and the structs methods have been lost to me by this point).

I have a working solution that simply enforces the pointer/interface's presence before doing any work but this feels like I am missing something. Given that the reflect.Value's reflect.Type can successfully find the interface surely there is something similar I can do on the reflect.Value side?

1

1 Answers

2
votes

The GetID method requires a pointer receiver, but you are operating on a struct value. Since that value isn't addressable, you can't call the GetID method, nor can you call Addr() to get a pointer to the value.

This is the same reason that this won't compile: https://play.golang.org/p/ZPQnVXtx01

hid := HasID{ID: "testID"}
_ = IHasID(hid)

You need to use a pointer to your original struct value when calling GetIDFrom

id, _ := GetIDFrom(reflect.ValueOf(&testy))

You should then be first checking if the pointer implements the interface, before dereferencing it with Elem() to check the struct type.