2 回答
TA贡献1851条经验 获得超5个赞
鉴于变量arr
已经实例化为 type [3]int
,我可以记住一些选项来覆盖其内容:
arr = [3]int{}
或者
arr = make([]int, 3)
0
它们都用's 的值覆盖切片。
请记住,每次我们使用此语法都会var := Type{}
将给定类型的新对象实例化到变量中。所以,如果你得到相同的变量并再次实例化它,你将把它的内容覆盖到一个新的变量上。
在 Go 中,语言将切片视为类型对象,而不是原始类型,如int
、rune
、byte
等。
TA贡献1851条经验 获得超3个赞
我从您的评论中看到您仍然不确定这里发生了什么:
我想分配一个数组一次,然后在 for 循环的迭代中重用它(当然在使用前清除)。你的意思是arr = [3]int{}重新分配而不是用 a 清除for i := range arr {arr[i] = 0}吗?
首先,让我们完全不考虑数组。假设我们有这个循环:
for i := 0; i < 10; i++ {
v := 3
// ... code that uses v, but never shows &v
}
这是否会在每次通过循环时创建一个新变量,或者这是否会在循环外创建一个变量,并且在每次通过循环时,坚持循环顶部的变量?语言本身并没有为您提供这个问题的答案。该语言描述了程序的行为,即每次初始化为 3,但如果我们从不观察,也许每次都相同。vv3v&v&v
如果我们选择观察&v,并在某个实现中实际运行这个程序,我们将看到实现的答案。现在事情变得有趣了。该语言表示,每个v(在循环中的新行程中分配的每个)都独立于任何先前的v. 如果我们采取&v,我们至少有一个潜在的能力,让每个 的实例v在随后的循环中仍然存在。所以每一个都v 不能干涉任何以前的变量v。编译器使用重新分配来保证每次重新分配它的简单方法。
当前的 Go 编译器使用转义分析来尝试检测是否某个变量的地址被占用。如果是这样,则该变量是堆分配的而不是堆栈分配的,并且运行时系统依赖(运行时)垃圾收集器来释放该变量。我们可以在 Go 操场上用一个简单的程序来演示这一点:
package main
import (
"fmt"
"runtime"
)
func main() {
for i := 0; i < 10; i++ {
if i > 5 {
runtime.GC()
}
v := 3
fmt.Printf("v is at %p\n", &v)
}
}
这个程序的输出不能保证是这样的,但这是我在运行它时得到的:
v is at 0xc00002c008
v is at 0xc00002c048
v is at 0xc00002c050
v is at 0xc00002c058
v is at 0xc00002c060
v is at 0xc00002c068
v is at 0xc000094040
v is at 0xc000094050
v is at 0xc000094040
v is at 0xc000094050
请注意,v一旦我们强制垃圾收集器开始运行,当i取值 6 到 10 时,地址如何开始重合(尽管交替)。那是因为v确实每次都会重新分配,但是通过运行 GC,我们之前做了一些-分配的,不再使用的内存再次可用。(确切地说,为什么这种交替是一个谜,但其行为可能取决于许多因素,例如 Go 版本、运行时启动分配、您的系统愿意使用多少线程等等。)
我们在这里展示的是 Go 的逃逸分析认为v逃逸,所以它每次都分配一个新的。我们传递&v给fmt.Printf,这就是让它逃脱的原因。未来的编译器可能会更聪明:它可能知道fmt.Printf不保存的值&v,因此变量在fmt.Printf返回后就死了,实际上并没有逃逸;&v在这种情况下,它可能会每次都重复使用。但是一旦我们在重要的地方添加了一些重要的东西,v真的会逃逸,编译器将不得不返回分别分配每一个。
关键问题是可观察性
除非你获取一个变量的地址——例如你的整个数组或它的任何元素——在 Go 中,你唯一能观察到的就是它的类型和值。一般来说,这意味着您无法判断编译器是否制作了某个变量的新副本,或者重用了旧副本。
如果将数组传递给函数,Go 会按 value传递整个数组。这意味着该函数不能更改数组中的原始值。我们可以通过编写一个实际上改变值的函数来了解这是如何观察到的:
package main
import (
"fmt"
)
func observe(arr [3]int) {
fmt.Printf("at start: arr = %v\n", arr)
for i, v := range arr {
arr[i] = v * 2
}
fmt.Printf("at end: arr = %v\n", arr)
}
func main() {
a := [3]int{1, 2, 3}
for i := 0; i < 3; i++ {
observe(a)
}
}
(游乐场链接)。
Go 中的数组是按值传递的,因此这不会更改in 中的数组a,main即使它确实更改了arrin 中的数组observe。
但是,我们通常希望更改数组并保留这些更改。为此,我们必须:
传递数组的地址,或
传递一个引用数组的切片
现在我们可以看到值的变化,即使我们从不查看各个地址,这些变化也会在函数调用中保留。语言说我们必须能够看到这些变化,所以我们可以;语言说,当我们通过值传递数组本身时,我们一定不能看到变化,所以我们不能。这取决于编译器想出某种方法来实现这一点。这是否涉及复制原始数组或其他一些神秘的魔法,取决于编译器——尽管 Go 试图成为一种简单的语言,其中“魔法”是显而易见的,显而易见的方法是复制或不复制,视情况而定.
这一切的重点
除了担心可观察的影响——即,我们是否首先计算出正确的答案——做所有这些实验的目的是证明编译器可以做任何它想做的事情,只要它产生正确的可观察的效果。
您可以尝试使编译器更容易,例如,通过分配单个数组,按地址(&a在上面的示例中)或切片(a[:]在上面的示例中)使用它,然后自己清除它。1 但这可能不会更快,甚至可能更慢,而不是只写你觉得最清楚的。先把它写清楚,然后计时。如果太慢,请尝试协助编译器,然后重新计时。您的协助可能会使事情变得更糟或没有效果:如果是这样,请不要打扰。如果它使事情变得更好,请保留它。
1知道您正在使用的编译器会进行转义分析,如果您想帮助它,您可以使用标志运行它,让它告诉您哪些变量已经转义。这些通常是优化的机会:如果您能找到防止变量转义的方法,编译器可以将其分配在堆栈上,而不是堆上,这可能会节省大量时间。但是,如果您的时间一开始并没有真正花在分配器上,那么这实际上也无济于事,因此分析通常是第一步。
- 2 回答
- 0 关注
- 309 浏览
添加回答
举报