使用嵌入在结构中的接口进行反射 - 如何检测 "real" 函数?

Go reflection with interface embedded in struct - how to detect "real" functions?

我现在的情况和这个帖子里问的一样:Meaning of a struct with embedded anonymous interface?

  type A interface {
     Foo() string
  }

  type B struct {
     A
     bar string
  }

习惯上,从 OOP 语言的背景来看,这个模式对我来说 "trying to say" 是 B 必须实现接口 A。但我现在明白了 "Go is different"。所以,而不是我最初期望的编译时检查,这很高兴编译有或没有

  func (B) Foo() string { .... }

现在。正如上面的问题所指出的(意译):"using embedded interfaces in structs is great for when you only want to implement /part/ of an interface"。

大概是因为此嵌入发生的情况与其他所有情况一样 - 类型 B 的值将具有类型 A 的匿名接口值,作为字段。就我个人而言,虽然我发现正交性令人欣慰,但我也发现反射包让我以这种方式直接从 B 的类型获取 A 的方法,而不是 error/nil 如果没有接收者 B 的方法,这让我感到困惑。但是 - 这个问题不是关于背后的想法 - 它是关于在 b := B{}:

之后如何初始化接口值
 func main() {
    bType := reflect.TypeOf(B{})
    bMeth, has := bType.MethodByName("Foo")
    if has {
      fmt.Printf("HAS IT: %s\n",bMeth.Type.Kind())
      res := bMeth.Func.Call([]reflect.Value{reflect.ValueOf(B{})})
      val := res[0].Interface()
      fmt.Println(val)
  } else {
      fmt.Println("DOESNT HAS IT")
  }
}

当这是 运行 时,它会引起可怕的恐慌

 HAS IT: func
 panic: runtime error: invalid memory address or nil pointer dereference

... 或不 - 取决于 compiler/runtime 是否能够找到上述方法。所以:如何在触发之前检测到这种情况?

也就是说 - 关于 bMeth 值,我可以使用它来查看在反射返回的方法和 func 值中没有 "real" 实现吗?更准确地说,它是类似于 "is the pointer to the function in the function table of the anonymous interface value in zero" 的东西,还是您从没有实现的反射接口中提取的方法到底发生了什么?

将整个事情包装在一个 goroutine 中并尝试 运行 defer/panic 下的函​​数不是答案——不仅因为 panic/defer 的开销,而且因为如果 存在,一般功能可能会产生副作用,我现在不想要...

我是否需要类似 运行 时间的实现来反映编译器的类型检查?或者有更简单的方法吗?我是不是想错了?

Above example in a Go playground

我认为这是不可能的。根据我在 reflect 的文档和 code, there is no way to know, whether a method is defined on the type or promoted 中看到的内容。似乎 panic-recover 是您在这里可以做的最好的事情。

你不用在我心里反思

method_in_table := B.Foo
fmt.Printf("%T \n", method_in_table)

会输出你

func(main.B) string

接口类型 A 在预先声明的 nil 处初始化,没有动态类型

var a A
if a==nil{
    fmt.Printf("It's nil")
}
a.Foo()

会给你同样的错误。所以实际检查可以只是

if b.A != nil { b.Foo()}

这里有3个问题。

  1. 嵌入式接口并不意味着"implements A"。它与嵌入任何其他类型的对象完全相同。如果要实现A,就做一个方法:func (b B) Foo() string.

    当你说:

    using embedded interfaces in structs is great for when you only want to implement /part/ of an interface

    确实可行,但您必须确保正确创建对象。把它想象成包装一个现有的对象:

    type MyReadCloser struct {
        io.ReadCloser
    }
    
    func (mrc *MyReadCloser) Read(p []byte) (int64, error) {
        // do your custom read logic here
    }
    
    // you get `Close` for free
    
    func main() {
        // assuming we have some reader
        var rc io.ReadCloser
        // you have to build the object like this:
        myReader := MyReadCloser{rc}
    }
    

    我不确定 Go 内部是如何做到的,但从概念上讲,它好像为您创建了一个 Close 方法:

    func (mrc *MyReadCloser) Close() error {
        return mrc.ReadCloser.Close()
    }
    
  2. 恐慌是因为Anil。如果你有:

    type concrete string
    func (c concrete) Foo() string {
        return string(c)
    }
    func main() {
        b := B{A: c("test")}
        // etc...
    }
    

    它会起作用的。换句话说,当你打电话时:

    bMeth.Func.Call([]reflect.Value{reflect.ValueOf(B{})})
    

    那是:

    B{}.Foo()
    

    即:

    B{}.A.Foo()
    

    Anil 所以你会感到恐慌。

  3. 关于如何仅获取对象直接实现的方法(而不是嵌入字段实现的方法)的问题,我无法看到使用 reflect 图书馆。 MethodByName 没有给出任何指示:

    <func(main.B) string Value>
    

    内部基本上是这样的函数:

    func(b B) string {
        return b.A.Foo()
    }
    

    而且我认为 reflect 中没有任何内容可以让您深入了解函数的内部结构。我尝试遍历字段,获取它们的方法并比较两者,但这也不起作用。

在您的问题得到很好的回答后,让我投入我的两分钱。

Presumably, this is because what is happening with this embed is just like in every other case - a value of type B would have an anonymous interface value of type A, as a field.

你基本上解决了这里的问题。这只是一个字段,但因为它是匿名的,所以它的所有方法都被提升,您可以直接在结构上使用它们。这不仅与接口有关,而且您指出的问题在普通结构中也存在:

package main

type A struct {
}

func (a A) Foo() {
}

type B struct {
    *A
}

func main() {
    B{}.Foo()
}

这会引起恐慌。我相信这是意料之中的:我们说的是 B 嵌入 *A,但随后将其保持未初始化状态,所以我在想什么?我们可以试着在这里找到一个类比,例如,C++,并发现它类似于 C++ 中的空指针——我们在那里如何处理它?我们要么期望它是非空的(根据合同),要么需要在使用前进行检查。后者是 Uvelichitel 在接受的答案中建议的,它绝不是正确的,我认为没有更好的解决方案。虽然这不是很合理。我们确实希望调用者知道他们正在调用的方法是匿名字段的提升方法,它是指针(或接口)类型,因此可以为 nil。作为此类代码的作者,我要么需要确保它永远不会为 nil(合同),要么在调用者需要检查它的文档中清楚地说明它(但为什么我会嵌入这种类型而不是普通字段,我是不确定)。

虽然接口让我很困扰,因为回顾你的例子并使 A 成为一个接口,我们有以下问题:

package main

import "fmt"

type A interface {
    Foo()
}

type B struct {
    A
}

func main() {
    var b interface{}
    b = &B{}

    // Nicely check whether interface is implemented
    if a, ok := b.(A); ok {
        a.Foo()
    }
}

糟糕,恐慌。我在这里明确不使用反射包来表明您的问题存在于 "normal" 语言使用中。我有一个接口对象 b,想检查它是否实现了接口 A。答案是肯定的,但我很恐慌。谁是罪魁祸首?如果说接口 b 后面的对象的创建者宣传了一些功能,但不关心提供实现,我会感到更加安慰。因此,我希望它称之为不良做法,或者至少强制在文档中明确说明,而不是假设上述类型断言中的 ok 实际上是好的。

我觉得它太长了而且跑题了。我对你的问题的回答是已经给出的答案的混合:直接检查 A is not null 如果不可能(你不知道推广该方法的确切领域),希望最好并责怪某人否则。

这个问题很老,有一些很好的答案,但是 none 提出了可以做到这一点的可能性。

在提出解决方案之前:我认为确保实现不会因为未能设置嵌入式接口字段而恐慌不是你的工作。有人可以传递一个实现,它明确定义了其中的方法 panic() 被显式调用。您无法检测到这种情况,但是,该实现不会比 nil 嵌入式接口字段更好。

好的,那么如何判断一个方法是否因为嵌入的接口字段是nil而导致实现不可用而导致恐慌而无法调用?

你说你不能/不想调用该方法并从恐慌中恢复,因为如果该方法可用,这将调用它并产生副作用。

事实是我们不必调用它。我们可以只通过实例(而不是类型)引用该方法,然后必须解析实际的接收者。当然,如果接收者是嵌入式接口的动态值,并且如果该接口是 nil,解析将导致运行时恐慌,但即使嵌入式接口不是 nil。请注意,这实际上是一个Method value,并且获取方法值评估并保存接收器。此接收器评估将失败。

让我们看一个例子:

type A interface {
    Foo() string
}

type B struct {
    A
}

func (b B) Int() int {
    fmt.Println("B.Int() called")
    return 0
}

func main() {
    b := B{}
    _ = b.Int
    fmt.Println("We got this far, b.Int is realized")
}

这个程序会输出什么?只有 "We got this far, b.Int is realized"。因为 Int() 方法是为 B 类型显式定义的,所以 b.Int 可以解析。因为它没有被调用,所以 "B.Int() called" 不会被打印出来。

如果我们这样做会怎样:

_ = b.Foo

由于 FooB.A 嵌入式接口的提升方法,而 b.Anil,解析 b.Foo 将在运行时失败,并产生一个运行时错误,像这样:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x47d382]

goroutine 1 [running]:
main.main()
    /tmp/sandbox877757882/prog.go:24 +0x2

但我们可以从中恢复:

defer func() {
    if r := recover(); r != nil {
        fmt.Println("Recovered:", r)
        fmt.Println("This means b.Foo is not realized!")
    }
}()
_ = b.Foo

这将输出:

Recovered: runtime error: invalid memory address or nil pointer dereference
This means b.Foo is not realized!

尝试 Go Playground 上的示例。