【问题标题】:How exactly are interface variables implemented in Go?Go 中的接口变量究竟是如何实现的?
【发布时间】:2023-03-06 00:37:01
【问题描述】:

在下面的代码 sn-p 中,我想了解在 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
}

(这个问题是我对我对why runtime error on io.WriterString? 的回答的准确事实正确性重新考虑的结果。所以我决定尝试做一些调查,以了解接口变量和对它们的分配究竟是如何工作的在 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 是否持有指针或值;似乎任何东西都可以在运行时为其分配任何一个而不会引发错误。为什么会做出这样的设计决定?它的用途是什么?它绝对不符合“类型安全”的心态。

【问题讨论】:

    标签: go pointers interface compilation


    【解决方案1】:

    所以,看起来在内部,接口变量确实保存了一个指向分配给它的指针。摘自http://research.swtch.com/interfaces

    接口值中的第二个词指向实际数据,在本例中是副本 b。赋值var s Stringer = b 复制b 而不是指向b,原因与var c uint64 = b 复制的原因相同:如果b 稍后更改,sc 应该具有原始值,而不是新值。

    我的问题

    [...] 当一个实现 IPerson 但具有不同大小/内存占用的对象被分配给 iPerson 时会发生什么?

    ...在文章中也得到了回答:

    存储在接口中的值可能是任意大的,但只有一个字专门用于保存接口结构中的值,因此赋值会在堆上分配一块内存并将指针记录在一个字槽中。

    所以,是的,堆上的一个副本被创建,一个指向它的指针分配给接口变量。但是,显然,对于程序员来说,接口变量具有值变量而不是指针变量的语义。

    (感谢 Volker 提供链接;而且,他的答案的第一部分实际上是完全错误的......所以我不知道我是否应该对误导性信息投反对票或对非误导性信息投赞成票和相当有用的链接(这也恰好与他自己的答案相矛盾)。)

    【讨论】:

      【解决方案2】:

      当你执行以下行时:

      iPerson = person
      

      您正在接口变量中存储Person 值。由于分配给结构执行副本,是的,您的代码正在复制。要从接口内部检索结构,您需要获取另一个副本:

      p := iPerson.(Person)
      

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

      iPerson = &person
      

      就幕后发生的事情而言,接口变量分配堆空间来存储大于指针的值是对的,但这通常对用户不可见。

      【讨论】:

      • 令我困惑的是,为什么 Go 同时允许 iPerson = &amp;personiPerson = person 而无需更改 iPerson 的类型。它允许可以静态捕获的运行时类型错误。这是沃尔克指出的文章没有涵盖甚至提到的内容。
      • 如果您将结构视为不可变的值,那么按值传递它可能很有意义。实际上,如果您将方法定义为采用指针接收器,您可能不会混淆它们,因为这些方法将无法访问接口值中保存的值,因此不会混淆:play.golang.org/p/E_8WjLS4S0跨度>
      • 好的,所以您是说在实践中这不会成为问题?但我仍然认为这是语言设计的不雅点,类型系统在这里可以做得更好。
      • 好吧,您将示例简化为一个空接口,该接口旨在根据定义接受任何内容。至于您关于类型断言中的恐慌的问题编辑,这是因为接口变量不包含 Person 值。如果您改为使用iPerson.(*Person),则可以成功检索该值。
      【解决方案3】:

      你问为什么两者都

      iPerson = person
      iPerson = &person
      

      是允许的。它们都是允许的,因为 person 和 &person 都实现了 IPerson 接口。这很明显,因为 IPerson 是空接口——每个值都实现了它。

      确实,您无法静态确定 IPerson 的值是持有指针还是持有值。所以呢?关于 IPerson,您所知道的只是存储在该类型值中的任何对象都实现了接口中的方法列表。假设是这些方法被正确实施。 IPerson 是否持有值或指针与此无关。

      例如,如果该方法要更改存储在对象中的某些内容,则该方法几乎必须是指针方法,在这种情况下,只能将指针值存储在接口类型的变量中。但是如果没有一个方法改变对象中存储的东西,那么它们都可以是值方法,并且可以在变量中存储非指针值。

      【讨论】:

        猜你喜欢
        • 2018-11-11
        • 2014-10-11
        • 2021-01-06
        • 1970-01-01
        • 2015-02-19
        • 1970-01-01
        • 2013-10-27
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多