[英]Go reflection with interface embedded in struct - how to detect “real” functions?
我现在的情况与在此线程中询问的情况相同: 具有嵌入式匿名接口的结构的含义?
type A interface {
Foo() string
}
type B struct {
A
bar string
}
习惯上,来自 OOP 语言的背景,这种模式对我来说“试图说”是 B 必须实现接口 A。但我现在明白“Go 是不同的”。 因此,与我最初预期的编译时检查不同,这很高兴在有或没有
func (B) Foo() string { .... }
展示。 正如上面的问题所指出的(释义):“在结构中使用嵌入式接口非常适合您只想实现接口的 /part/ ”。
据推测,这是因为这个嵌入发生的事情就像在所有其他情况下一样 - 类型 B 的值将具有类型 A 的匿名接口值作为字段。 就我个人而言,虽然我发现正交性令人欣慰,但我也发现反射包会让我以这种方式直接从 B 的类型获取 A 的方法,如果不存在接收器 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
...或不- 取决于编译器/运行时是否能够找到上述方法。 那么:如何在触发之前检测到这种情况?
也就是说 - 是否有关于 bMeth 值的一些信息,我可以使用它来查看反射返回的返回 Method 和 func 值中不存在“真实”实现? 这是否更准确地说是“匿名接口值的函数表中函数的指针为零”,或者从没有实现的带有反射的接口中提取的方法究竟发生了什么?
将整个事情包装在一个 goroutine 中并尝试在 defer/panic 下运行该函数并不是答案——不仅是因为 panic/defer 的开销,而且因为该函数通常可能(如果它确实存在)有副作用我现在不想...
我是否想要一个类似于编译器类型检查的运行时实现? 或者有更简单的方法吗? 我是否错误地考虑了这一点?
你不需要在我脑海里反省
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()}
这个问题很老,有一些很好的答案,但没有一个提出可以做到这一点的可能性。
在提出解决方案之前:我认为确保实现不会因为无法设置嵌入式接口字段而恐慌不是你的工作。 有人可以传递一个实现,该实现显式定义了显式调用panic()
的方法。 您无法检测到这种情况,但是,该实现不会比nil
嵌入式接口字段更好。
好的,那么如何判断一个方法是否不能被调用,因为它会因为嵌入的接口字段为nil
导致实现不可用而导致恐慌?
你说你不能/不想调用该方法并从恐慌中恢复,因为如果该方法可用,这将调用它并产生副作用。
事实是我们不必调用它。 我们可以通过实例(而不是类型)来引用方法,然后必须解析实际的接收者。 当然,如果接收者是嵌入式接口的动态值,并且该接口为nil
,则解析将导致运行时恐慌,但即使嵌入式接口不是nil
也不会调用该方法。 请注意,这实际上是一个Method value ,并且获得一个 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.Int() called"
。
如果我们这样做会怎样:
_ = b.Foo
由于Foo
是BA
嵌入式接口的提升方法,而bA
为nil
,解析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上的例子。
在你的问题已经得到很好的答案之后,让我投入我的两分钱。
据推测,这是因为这个嵌入发生的事情就像在所有其他情况下一样 - 类型 B 的值将具有类型 A 的匿名接口值作为字段。
你已经基本上解决了这里的问题。 这只是一个字段,但因为它是匿名的,所以它的所有方法都被提升,你可以直接在结构上使用它们。 这不仅与接口有关,而且您指出的问题也存在于普通结构中:
package main
type A struct {
}
func (a A) Foo() {
}
type B struct {
*A
}
func main() {
B{}.Foo()
}
这会引起恐慌。 我相信这是意料之中的:我们说B
嵌入了*A
,然后让它未初始化,那么我在想什么? 我们可以尝试在这里找到一个类比,例如,C++ 并发现它类似于 C++ 中的空指针——我们如何处理它? 我们要么期望它是非空的(通过合同),要么需要在使用前检查。 后者是 Uvelichitel 在接受的答案中建议的,它绝不是正确的,我认为没有更好的解决方案。 虽然不太靠谱。 我们确实希望调用者知道他们正在调用的方法是匿名字段的提升方法,该字段是指针(或接口)类型,因此可以为零。 作为此类代码的作者,我需要确保它永远不会为零(合同)或在文档中明确说明调用者需要检查它(但为什么我要嵌入这种类型而不是普通字段,我是没有把握)。
不过,它让我对接口感到困扰,因为回顾您的示例并将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()
}
}
哎呀,恐慌。 我在这里明确不使用反射包来表明您的问题存在于“正常”语言使用中。 我有一个接口对象b
并想检查它是否实现了接口A
。 答案是肯定的,但我越来越恐慌。 谁是罪魁祸首? 如果说接口b
背后的对象创建者宣传了一些功能,但并不关心提供实现,我会感到更加安慰。 因此我想它称之为一种不好的做法,或至少迫使它在文件中清楚地说明,而不是假设ok
的上述类型的断言手段实际确定。
我认为它变得太长而且离题了。 我对你的问题的回答是已经给出的答案的混合:直接检查A
不为空,如果不可能(你不知道推广该方法的确切领域),希望最好并责怪别人。
这里有3个问题。
嵌入式接口并不意味着“实现 A”。 这与嵌入任何其他类型的对象完全相同。 如果要实现 A,只需创建一个方法: func (b B) Foo() string
。
当你说:
当您只想实现接口的 /part/ 时,在结构中使用嵌入式接口非常有用
这确实有效,但您必须确保正确创建对象。 把它想象成包装一个现有的对象:
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() }
恐慌是因为A
nil
。 如果你有:
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()
A
是nil
所以你会感到恐慌。
关于如何仅获取对象直接实现的方法(而不是嵌入字段实现的方法)的问题,我无法看到使用reflect
库的方法。 MethodByName
没有给出任何指示:
<func(main.B) string Value>
在内部,这基本上是一个这样的函数:
func(b B) string { return bAFoo() }
我认为在reflect
中没有任何内容可以让您查看函数的内部结构。 我尝试遍历字段,获取它们的方法并比较两者,但这也不起作用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.