在Go语言中,接口变量是如何实现的?

13
在下面的代码片段中,我想了解当其内容仍未初始化时iPerson存储了什么:只是0字节的值吗?还是它实际上是一个指针(当然也初始化为0字节)?无论如何,在iPerson = person发生了什么?
如果iPerson = person复制了person,那么当分配给iPerson一个具有不同尺寸/内存占用的实现IPerson对象时会发生什么呢?我理解iPerson是存储在堆栈上的变量,因此其大小必须固定。这是否意味着堆实际上在背后被使用,所以iPerson实际上是作为指针实现的,但分配仍然复制对象,正如以上代码所示?
以下是代码:
type Person struct{ name string }

type IPerson interface{}

func main() {
    var person Person = Person{"John"}
    var iPerson IPerson
    fmt.Println(person)  // => John
    fmt.Println(iPerson) // => <nil>  ...so looks like a pointer

    iPerson = person     //           ...this seems to be making a copy
    fmt.Println(iPerson) // => John

    person.name = "Mike"
    fmt.Println(person)  // => Mike
    fmt.Println(iPerson) // => John   ...so looks like it wasn't a pointer,
                         //           or at least something was definitely copied
}

(这个问题是我对我在为什么 io.Writer.String() 运行时错误的精确事实正确性的回答产生了犹豫。所以我决定尝试进行一些调查,了解Go语言中接口变量和赋值工作的具体情况。) 编辑:在收到几个有用的答案后,我仍然感到困惑:
iPerson = person
iPerson = &person

两者都是合法的。然而,对我来说,这引出了一个问题:为什么编译器允许出现如此弱类型的情况?上述情况的一个含义是:

iPerson = &person
var person2 = iPerson.(Person)  # panic: interface conversion: interface is *main.Person, not main.Person

改变第一行可以解决这个问题:
iPerson = person
var person2 = iPerson.(Person)  # OK

因此,静态地确定iPerson是持有指针还是值是不可能的;似乎任何东西都可以在运行时将任何一种类型赋值给它,而不会引发任何错误。为什么要做出这样的设计决策?它有什么作用?它显然不符合“类型安全”的思维方式。

3个回答

9
你问为什么两个
iPerson = person
iPerson = &person

允许使用 person 和 &person,因为它们都实现了 IPerson 接口。很明显,IPerson 是一个空接口,因此每个值都可以实现它。

确实,您无法静态确定 IPerson 值存储的是指针还是值。但这有什么关系呢?您只需知道 IPerson 中存储的任何对象都实现了该接口中的方法列表。假设这些方法已正确实现。IPerson 存储的是值还是指针对此无关紧要。

例如,如果该方法应更改存储在对象中的某些内容,则该方法必须是指针方法,因此只能将指针值存储在接口类型的变量中。但如果没有任何方法更改存储在对象中的内容,则它们都可以是值方法,并且可以将非指针值存储在变量中。


7
看起来,接口变量在内部确实保存了指向被分配给它的内容的指针。以下是http://research.swtch.com/interfaces中的一段摘录:
“接口值中的第二个字指向实际数据,在这种情况下是 b 的副本。赋值 var s Stringer = b 会复制 b 而不是指向 b,原因与 var c uint64 = b 复制 b 相同:如果 b 后来更改,则 s 和 c 应具有原始值,而不是新值。”
我的问题在下面得到回答:
“存储在接口中的值可能是任意大的,但只有一个字用于在接口结构中保存值,因此赋值会在堆上分配一块内存并记录指针在一个字的位置。”
因此,是在堆上创建了一个副本,并将指向该副本的指针分配给接口变量。但是,对于程序员来说,接口变量具有值变量而不是指针变量的语义。
(感谢 Volker 提供链接;但是,他回答的第一部分事实上是错误的……所以我不知道是否应该因为误导性信息而投反对票,还是因为提供了非误导性且相当有用的链接而投赞成票(该链接也恰好与他自己的回答相矛盾)。)

6
当您运行以下代码时:
iPerson = person

您正在将一个Person值存储在接口变量中。由于结构体赋值会执行一次复制,因此您的代码确实进行了复制。要从接口中检索结构体,您需要再进行一次复制:

p := iPerson.(Person)

所以,您很少想要对可变类型使用这种方法。如果您希望在接口变量中存储指向结构体的指针,则需要显式执行此操作:

iPerson = &person

就底层实现而言,您是正确的,接口变量会分配堆空间来存储比指针更大的值,但通常用户看不到这一点。


让我困惑的是为什么Go允许iPerson = &personiPerson = person两种方式,而不需要改变iPerson的类型。这样会导致运行时类型错误,而这些错误本可以在静态时期被捕获。这是Volker提出的文章没有涉及或者提到的问题。 - Erik Kaplun
如果您将struct视为不可变值,则按值传递可能是非常合理的。实践中,如果您定义方法以使用指针接收器,则可能不会混淆它们,因为这些方法将无法访问接口值中保存的值,从而不会产生混淆: http://play.golang.org/p/E_8WjLS4S0 - James Henstridge
好的,所以您在说实践中这不会成为一个问题?但我仍然认为这是语言设计上的一个不够优雅之处,类型系统在这方面可以做得更好。 - Erik Kaplun
你将你的示例简化为一个空接口,这意味着它根据定义可以接受任何东西。至于你关于类型断言中 panic 的问题编辑,那是因为接口变量不包含 Person 值。如果你改为使用 iPerson.(*Person),你就可以成功地检索到该值。 - James Henstridge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接