How exactly are interface variables implemented in Go?

huangapple go评论82阅读模式
英文:

How exactly are interface variables implemented in Go?

问题

在下面的代码片段中,我想了解当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>  ...所以看起来是一个指针

	iPerson = person     //           ...这似乎是在复制
	fmt.Println(iPerson) // => John

	person.name = "Mike"
	fmt.Println(person)  // => Mike
	fmt.Println(iPerson) // => John   ...所以看起来它不是一个指针,
	                       //           或者至少肯定复制了某些东西
}

(这个问题是我对https://stackoverflow.com/questions/19196653/why-runtime-error-on-io-writerstring/19196719#19196719的答案的准确性产生了疑问。所以我决定进行一些调查,以了解在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是指针还是值;而且似乎任何东西都可以在运行时将任一类型赋值给它而不引发错误。为什么会做出这样的设计决策?它有什么用途?它显然不符合“类型安全”的思维方式。

英文:

In the below code snippet, I'd like to understand what exactly gets stored in iPerson when its contents are still uninitialized: just a value of 0-bytes? Or is it actually a pointer under the hood (and also initialized to 0-bytes of course)? In any case, what exactly happens at iPerson = person?

If iPerson = person makes a copy of person, what happens then when an object implementing IPerson but with a different size/memory footprint gets assigned to iPerson? I understand iPerson is a variable stored on the stack, so its size must be fixed. Does that mean that the heap is actually used under the hood, so iPerson is actually implemented as a pointer, but assignments still copy the object, as demonstrated by the above code?
Here's the code:

type Person struct{ name string }
 
type IPerson interface{}
 
func main() {
 	var person Person = Person{&quot;John&quot;}
 	var iPerson IPerson
 	fmt.Println(person)  // =&gt; John
 	fmt.Println(iPerson) // =&gt; &lt;nil&gt;  ...so looks like a pointer
 
 	iPerson = person     //           ...this seems to be making a copy
 	fmt.Println(iPerson) // =&gt; John
 
 	person.name = &quot;Mike&quot;
 	fmt.Println(person)  // =&gt; Mike
 	fmt.Println(iPerson) // =&gt; John   ...so looks like it wasn&#39;t a pointer,
                         //           or at least something was definitely copied
}

(This question is the result of me having second thoughts on the precise factual correctness of my answer to https://stackoverflow.com/questions/19196653/why-runtime-error-on-io-writerstring/19196719#19196719. So I decided to try to do some investigation to understand how is it exactly that interface variables and assignments to them work in Go.)

EDIT: after having received a few useful answers, I'm still puzzled with this:

iPerson = person
iPerson = &amp;person

—both are legal. However, to me, this raises the question of why the compiler allows such weak typing to occur? One implication of the above is this:

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

whereas changing the first line fixes it:

iPerson = person
var person2 = iPerson.(Person)  # OK

...so it's not possible to determine statically whether iPerson holds a pointer or a value; and it seems that anything can assign either one to it at runtime with no errors raised. Why was such design decision made? What purpose does it serve? It definitely does not to fit within the "type safety" mindset.

答案1

得分: 9

你问为什么同时允许使用以下两种方式:

iPerson = person
iPerson = &person

这两种方式都是允许的,因为person和&person都实现了IPerson接口。这是显而易见的,因为IPerson是一个空接口,每个值都实现了它。

确实,你无法在静态上确定IPerson的值是指针还是值。那又怎样呢?你只知道IPerson中存储的对象实现了接口中的方法列表。假设这些方法都被正确实现了。IPerson是存储值还是指针对此并不重要。

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

英文:

You ask why both of

iPerson = person
iPerson = &amp;person

are permitted. They are both permitted because both person and &person implement the IPerson interface. This is obvious, because IPerson is the empty interface--every value implements it.

It's true that you can't determine statically whether a value of IPerson holds a pointer or a value. So what? All you know about IPerson is that any object stored in a value of that type implements the list of methods in the interface. The assumption is that those methods are implemented correctly. Whether IPerson holds a value or a pointer is irrelevant to that.

For example, if the method is supposed to change something stored in the object, then that the method pretty much has to be a pointer method, in which case only a pointer value can be stored in the variable of interface type. But if none of the methods change something stored in the object, then they can all be value methods, and a non-pointer value can be stored in the variable.

答案2

得分: 7

所以,从内部来看,接口变量确实保存了指向其分配的内容的指针。来自http://research.swtch.com/interfaces的摘录:

接口值的第二个字指向实际数据,在这种情况下是b的一个副本。赋值var s Stringer = b会创建b的一个副本,而不是指向b,原因与var c uint64 = b创建副本的原因相同:如果b后来发生了变化,sc应该保留原始值,而不是新值。

我的问题

[...] 当实现IPerson接口的对象的大小/内存占用不同时,将一个这样的对象分配给iPerson时会发生什么?

...在文章中也有答案:

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

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

(感谢Volker提供的链接;但是,他回答的第一部分在事实上是完全错误的...所以我不知道是应该因为误导性信息而给他点踩,还是因为非误导性且相当有用的链接而给他点赞(这也恰好与他自己的回答相矛盾)。)

英文:

So, looks like internally, the interface variable does hold a pointer to what was assigned to it. An excerpt from http://research.swtch.com/interfaces:

> The second word in the interface value points at the actual data, in this case a copy of b. The assignment var s Stringer = b makes a copy of b rather than point at b for the same reason that var c uint64 = b makes a copy: if b later changes, s and c are supposed to have the original value, not the new one.

My question

> [...] what happens then when an object implementing IPerson but with a different size/memory footprint gets assigned to iPerson?

...also gets answered in the article:

> Values stored in interfaces might be arbitrarily large, but only one word is dedicated to holding the value in the interface structure, so the assignment allocates a chunk of memory on the heap and records the pointer in the one-word slot.

So yeah, a copy on the heap is made and a pointer to it assigned to the interface variable. But, apparently, to the programmer, the interface variable has the semantics of a value variable not a pointer variable.

(Thanks to Volker for providing the link; but also, the first part of his answer is factually plain wrong... So I don't know if I should downvote for the misleading information or upvote for the non-misleading and rather useful link (which also happens to contradict his own answer).)

答案3

得分: 6

当你执行以下代码行时:

iPerson = person

你将一个 Person 值存储在接口变量中。由于对结构体的赋值会执行一次拷贝,所以你的代码确实进行了一次拷贝。要从接口内部检索结构体,你需要再进行一次拷贝:

p := iPerson.(Person)

因此,你很少会在可变类型上这样做。如果你想在接口变量中存储结构体的指针,你需要显式地进行如下操作:

iPerson = &person

关于底层的实现,你是对的,接口变量会分配堆空间来存储大于指针大小的值,但这通常对用户来说是不可见的。

英文:

When you execute the following line:

iPerson = person

You are storing a Person value in the interface variable. Since assignment to a struct performs a copy, yes your code is taking a copy. To retrieve the struct from inside the interface you'll need to take another copy:

p := iPerson.(Person)

so you'd rarely want to do this with mutable types. If you instead want to store a pointer to the struct in the interface variable, you need to do this explicitly:

iPerson = &amp;person

As far as what goes on under the hood, you are right that interface variables allocate heap space to store values larger than a pointer, but this is usually not visible to the user.

huangapple
  • 本文由 发表于 2013年10月7日 18:12:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/19222133.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定