Go 在这里非常传统。您可以将系统的整体内存想象成一个容纳小盒子的大盒子。每个小盒子也可以装东西。
声明一个变量:
var i int
告诉 Go 系统它应该在某处分配一个小盒子——你无法控制 where,因为这取决于 Go 系统——它大到足以容纳一些 int 值.由于我们还没有将任何值放入框中,Go 将其预填充为零:
i:
+-----+
| 0 |
+-----+
如果我们将 i 设置为 5,则将 5 放入框内。使用 i := 5 是告诉 Go 编译器的一种简写方式:在某处创建变量 i 并将 5 放入框中全部放在一行中,这样我们得到:
i:
+-----+
| 5 |
+-----+
所以你现在有了这个设置,因为你结合了声明和赋值,编译器很容易避免任何浪费的努力:它不必先将i设置为零,然后将i设置为5.(当然,编译器可以很聪明,避免浪费精力,即使你没有使用简短的声明形式,但我们人类不必考虑这一点更好:简短的声明形式让我们可以将其视为创建i,并将其设置为5。)
我们还有:
var d *int
告诉 Go 系统分配一个足够大的盒子来容纳 *int 值。这可能与int 值的大小相同,或者更大或更小。我会把它画出两倍大,你的系统可能是这样,也可能不是:
d:
+-----------+
| nil |
+-----------+
如果我们还没有设置d,Go 会用适当的零值填充它,即nil。
如果我们现在将d 设置为指向i,这将更改d,使其指向i:
d: i:
+-----------+ +-----+
| *-----------> | 5 |
+-----------+ +-----+
在这里,您使用d := &i 声明d,然后设置 d 为&i,所以我们现在有这种情况(没有任何努力浪费在设置@987654347先将 @ 设为 nil,然后将 nil 也替换为 &i 的值。
您对fmt.Printf 的前两次调用传递,首先是值&i,然后是存储在d 中的值,这也是&i,所以这两个值打印出来是一样的。 (请注意,%d 通常不是打印指针值的好方法,尽管 fmt 的 Go 包规范说它会起作用。对于现代计算机,通常最好以十六进制打印指针;%p 格式这样做。)
您的最后一次调用传递了值&d。我们还没有为此绘制一个记忆框,但是为了传递这个值,Go 必须将它塞进某个盒子的某个地方。1我们现在可以绘制它了:
unnamed:
+-----------+
| * |
+-----|-----+
|
d: v i:
+-----------+ +-----+
| *-----------> | 5 |
+-----------+ +-----+
所以 值 &d 位于内存中的某个盒子中,位于没有名称的位置(d 和 i 位于确实有名称的位置)可以用来谈论它们)。在那个盒子里面是一个指针值。指针值指向包含d 的框。
对fmt.Printf 的最后一次调用将这个指针值(这个&d 值,存储在未命名的框中)发送到fmt.Printf,以便fmt.Printf 可以打印它。这个值当然必须不同于存储在d 框中的值,因为d 指向i。所以最终打印出来的值不一样。
这个程序总体上有一组可能的正确答案。我们不知道打印的三个值是什么,但我们确实知道第一个和第二个值将是相同,而第三个打印值将 em>不匹配前两个。如果这没有发生,则您使用的系统没有正确实现 Go 语言。2
1如果有一种快速传递值的方法不涉及使用内存盒,Go 可以这样做,只要你不知道,在正常的语言规则内,Go 正在这样做。但同样适用于普通变量!
Go 编译器和运行时系统只需要生成正确答案,不管它应用了多少编译时魔法来产生正确答案。此外,“正确答案”仅针对可能程序的某些子集定义。
2这可能有点夸大了。我不确定 Go 是否禁止压缩垃圾收集器在内存中移动对象,更新所有指针。实际现有的 Go 系统没有压缩收集器,但是可以通过使用这样的代码来判断这一点,这至少有点可疑。如果 Go 允许 允许压缩收集器,则指针的打印值可能会发生变化,因此打印的前两个值可能会不同。