Search code examples
goreflection

Golang: how to determine a method is promoted from an embedded struct instead of being directly implemented


How can I tell if a method/interface is promoted from an embedded struct (in other words: a struct "inherits" a method instead of directly implementing it)?

For example: is there a way to determine Main does not directly implement the interface Interface given this?:

type Interface interface {
  Method()
}

type Embedded struct{}
func (t *Embedded) Method() {}

type Main struct {
  Embedded
}

... as opposed to the case where it does directly implement the interface:

type Main struct {
  Embedded
}
func (t *Main) Method() {} // this is directly implemented

Tried:

Standard type assertion in go results in Main having the Method in both examples, implementing Interface, as expected:

main := &Main{}
_, implementsInterface := main.(Interface) // implementsInterface is true

Reflection also indicates an instance of Main has the method in both examples:

main := &Main{}

mainVal := reflect.ValueOf(main)
mainMethod, hasMainMethod := mainVal.Type().MethodByName("Method") // hasMainMethod is true

embedded := val.Elem().Field(0)
embeddedMethod, hasEmbeddedMethod := embedded.Type().MethodByName("Method") // hasEmbeddedMethod is true

// mainMethod != embeddedMethod
// pointers in the methods are not the same
// is there some other way to determine these methods are "the same"?

Solution

  • The compiler will create a wrapper for a promoted method. So in the first place, the promoted method is different from the original method.

    On the other hand, this fact gives us a not-so-reliable workaround: if a method is a wrapper, it could be a promoted method. And a still not-so-reliable way to check if a method is a compiler-generated wrapper is to check the file name of the source code corresponding to the method. For a wrapper, the file name will be <autogenerated>.

    Let's see a demo first:

    package main
    
    import (
        "fmt"
        "reflect"
        "runtime"
    )
    
    type inner struct{}
    
    func (t *inner) Method() {}
    
    type outer1 struct{ inner }
    
    type outer2 struct{ inner }
    
    func (o *outer2) Method() {}
    
    func isPromoted(s any, methodName string) (bool, error) {
        v := reflect.ValueOf(s)
        m, ok := v.Type().MethodByName(methodName)
        if !ok {
            return false, fmt.Errorf("method sets of s does not include the method")
        }
    
        p := m.Func.Pointer()
        f := runtime.FuncForPC(p)
    
        fileName, _ := f.FileLine(f.Entry())
    
        promoted := fileName == "<autogenerated>"
        return promoted, nil
    }
    
    func main() {
        i := &inner{}
        o1 := &outer1{inner: *i}
        o2 := &outer2{inner: *i}
    
        fmt.Println("i - is [Method] promoted?")
        fmt.Println(isPromoted(i, "Method"))
    
        fmt.Println("o1 - is [Method] promoted?")
        fmt.Println(isPromoted(o1, "Method"))
    
        fmt.Println("o2 - is [Method] promoted?")
        fmt.Println(isPromoted(o2, "Method"))
    }
    

    And the output (see https://go.dev/play/p/XUIE0ws3eFN):

    i - is [Method] promoted?
    false <nil>
    o1 - is [Method] promoted?
    true <nil>
    o2 - is [Method] promoted?
    false <nil>
    

    This workaround is not-so-reliable because:

    1. a wrapper is not necessary a promoted method. For example, when passing i.Method as a parameter to a function, a wrapper is created (follow the link above to the playground to see a demo). I'm not sure whether there are other cases.

    2. the demo uses (v Value).Pointer. According to the doc:

      If v's Kind is Func, the returned pointer is an underlying code pointer, but not necessarily enough to identify a single function uniquely.

      And the comment in the implementation:

      As the doc comment says, the returned pointer is an underlying code pointer but not necessarily enough to identify a single function uniquely. All method expressions created via reflect have the same underlying code pointer, so their Pointers are equal.

      Does isPromoted implemented in the demo work for methods created via reflect? It's not tested.

    3. <autogenerated> is an implementation detail and is not documented. It could be changed any time.