登录
首页 >  Golang >  Go问答

在 Go 中如何进行数组的复用和重置操作?

来源:stackoverflow

时间:2024-03-18 22:27:29 372浏览 收藏

Go 语言中,数组可以按值传递或通过地址传递。按值传递的数组无法被函数修改,而通过地址传递的数组则可以。为了修改数组,可以传递数组的地址或引用数组的切片。数组的内容可以通过重新分配或使用 for 循环逐个元素地进行覆盖。Go 语言的编译器会进行逃逸分析,以确定变量是否需要在堆上分配,如果变量没有逃逸,则可以将其分配在堆栈上以提高性能。

问题内容

如何在 go 中清除并重用数组? 将所有值分配给默认值的手动 for 循环是唯一的解决方案吗?

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

解决方案


我从your comment看到你仍然不确定这里发生了什么:

我想分配一个数组一次,然后在 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
}

这是否会在循环的每次行程中创建一个变量 v,还是在循环外部创建一个变量 v 并在循环的每次行程中将 3 粘贴到顶部的变量中循环的? 语言本身无法为您提供这个问题的答案。该语言描述了程序的行为,即v每次都被初始化为3,但如果我们从不观察&v,也许&v每次都是相同

如果我们选择观察 &v,并在某个实现中实际运行该程序,我们将看到实现的答案。现在事情变得有趣了。 语言表示每个 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 版本、运行时启动分配、系统愿意使用多少个线程等等。) p>

这里我们展示的是,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但这可能不会比直接写出你认为最清晰的内容更快,甚至可能。先把它写清楚,然后再计时。如果太慢,请尝试协助编译器,然后重新计时。您的协助可能会使事情变得更糟或没有效果:如果是这样,请不要打扰。如果它让事情变得更好,就保留它。

1知道您使用的编译器会进行转义分析,如果您想帮助它,您可以使用标志来运行它,让它告诉您哪些变量已转义。这些通常是优化的机会:如果您能找到一种方法来防止变量逃逸,编译器可以将其分配在堆栈上,而不是堆上,这可能会节省大量有用的时间。但是,如果您的时间一开始并没有真正花在分配器上,那么这实际上不会有任何帮助,因此分析通常是第一步

考虑到这一点变量 arr 已经实例化为 [3]int 类型,我记得一些覆盖其内容的选项:

arr = [3]int{}

arr = make([]int, 3)

两者都用 0 的值覆盖切片。

请记住,每次我们使用此语法 var := type{} 都会将给定 type 的新对象实例化为变量。因此,如果您获得相同的变量并再次实例化它,您将把它的内容覆盖为新的内容。

在 go 中,语言将切片视为类型对象,而不是像 intrunebyte 等原始类型。

今天关于《在 Go 中如何进行数组的复用和重置操作?》的内容介绍就到此结束,如果有什么疑问或者建议,可以在golang学习网公众号下多多回复交流;文中若有不正之处,也希望回复留言以告知!

声明:本文转载于:stackoverflow 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>