如何在Go语言中清除和重用数组(而不是切片)?

m1m5dgzv  于 2023-08-01  发布在  Go
关注(0)|答案(2)|浏览(90)

如何在Go中清除和重用数组?手动循环并将所有值赋为默认值是唯一的解决方案吗?

package main

import (
    "fmt"
)

func main() {
    arr := [3]int{1,2,3}
    fmt.Println(arr) // Output: [1 2 3]

    // clearing an array - is there a faster/easier/less verbose way?
    for i := range arr {
        arr[i] = 0
    }
    
    fmt.Println(arr) // Output: [0 0 0]
}

字符串

lokaqttq

lokaqttq1#

假设变量arr已经示例化为类型[3]int,我可以记住一些选项来覆盖它的内容:

arr = [3]int{}

字符串

arr = make([]int, 3)


它们都用0的值覆盖切片。
请记住,每次我们使用此语法var := Type{}时,都会将给定Type的新对象示例化到变量。所以,如果你得到相同的变量,并再次示例化它,你将覆盖它的内容到一个新的。
在Go中,语言将切片视为类型对象,而不是intrunebyte等原始类型。

4ngedf3f

4ngedf3f2#

我从你的评论中看到,你仍然不确定这里发生了什么:
我想分配一次数组,然后在for循环的迭代中重用它(当然是在使用前清除)。你的意思是说,arr = [3]int{}进行重新分配,而不是用for i := range arr {arr[i] = 0}进行清算?
首先,让我们把数组完全抛到脑后。假设我们有这个循环:

for i := 0; i < 10; i++ {
    v := 3
    // ... code that uses v, but never shows &v
}

字符串
这是在每次循环中创建一个 new 变量v,还是在循环外创建一个变量v,并在每次循环中将3插入循环顶部的变量?语言本身并不能回答这个问题。该语言描述了程序的 * 行为 *,即v每次都被初始化为3,但如果我们从来没有观察到&v,也许&v每次都是 * 相同的 *。
如果我们选择观察&v,并在某个实现中实际运行这个程序,我们将看到 * 实现的答案 *。现在事情变得有趣了。language 表示每个v--在循环的新行程中分配的每个v--独立于任何先前的v。如果我们取&v,那么v的每个示例至少都有一个潜在的能力,在后续的循环中仍然是活动的。因此,每个v * 必须不 * 干扰任何 * 前 * 变量v。编译器保证这一点的简单方法是每次重新分配它,使用重新分配。
当前的Go编译器使用escape analysis来尝试检测某个变量的地址是否被占用。如果是,则该变量是堆分配的而不是堆栈分配的,并且运行时系统依赖于(运行时)垃圾收集器来释放该变量。我们可以用一个简单的程序on the Go playground来证明这一点:

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


请注意,当i的值为6到10时,一旦我们强制垃圾收集器开始运行,v的地址就开始重合(尽管是交替的)。这是因为v确实每次都被重新分配,但是通过运行GC,我们使一些以前分配的、不再使用的内存再次可用。(确切地说,为什么这种交替是一个谜,但行为可能取决于许多因素,如Go版本,运行时启动分配,你的系统愿意使用多少线程,等等。
我们在这里展示的是,Go的转义分析认为v转义了,所以它每次都分配一个新的。我们将&v传递给fmt.Printf,这就是让它逃逸的原因。未来的编译器可能会更聪明:它可能知道fmt.Printf没有保存&v的值,因此变量在fmt.Printf返回后是死的,并且实际上没有转义;在这种情况下,它可能每次都重用&v。但是,一旦我们添加了一些重要的东西,v真的会 * 逃逸,编译器将不得不返回单独分配每个。

关键问题是可观察性

除非你获取一个变量的地址--比如你的整个数组或它的任何元素--在Go语言中,你唯一能观察到的就是它的类型和值。一般来说,意味着你不能判断编译器是否已经为某个变量创建了一个新的副本,或者重用了一个旧的副本。
如果你将一个数组传递给一个函数,Go会通过值传递整个数组。这意味着该函数不能更改数组中的原始值。我们可以通过编写一个实际上改变值的函数来观察这一点:

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)
    }
}


playground link)。
Go语言中的数组是通过值传递的,所以这不会 * 改变 * main中的数组a,即使它确实改变了observe中的数组arr
但是,我们经常希望更改数组并保留这些更改。为此,我们必须:

现在我们可以看到值的变化,这些变化在函数调用中保留下来,即使我们从来没有看过各种地址。语言说,我们必须能够看到这些变化,所以我们能够;语言说,当我们通过值传递数组本身时,我们必须 * 不能 * 看到变化,所以我们不能。这取决于编译器想出某种方法来实现这一点。这是否涉及复制原始数组,或者其他一些神秘的魔法,这取决于编译器-尽管Go试图成为一种简单的语言,其中“魔法”是显而易见的,显而易见的方式是复制,或者不复制,视情况而定。

这一切的重点

除了担心可观察到的效果,即我们是否首先计算出了正确的答案,做所有这些实验的目的是表明编译器可以做任何它想做的事情,只要它产生正确的可观察到的效果。

你可以尝试让编译器更容易,例如,分配一个数组,按地址(上面的例子中的&a)或切片(上面的例子中的a[:])使用它,然后自己清除它。先写清楚,然后计时。如果它太慢,尝试协助编译器,并再次计时。你的帮助可能会使事情变得更糟或没有效果:如果是这样,就不要麻烦了。如果这能让事情变得更好,那就留着吧。
1知道你正在使用的编译器进行转义分析,如果你想帮助它,你可以运行它的标志,让它告诉你哪些变量已经转义。这些通常是优化的机会:如果你能找到一种方法来防止变量转义,编译器可以在堆栈上分配它,而不是在堆上,这可能会保存大量的时间。但是如果你的时间一开始就没有真正花在分配器上,这实际上也不会有什么帮助,所以分析通常是第一步。

相关问题