Go切片操作符是否会分配新的底层数组?

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

Does Go slicing operator allocate new underlying array?

问题

我正在为您翻译以下内容:

我正在阅读一篇解释Go语言切片实现原理的文章:
https://medium.com/swlh/golang-tips-why-pointers-to-slices-are-useful-and-how-ignoring-them-can-lead-to-tricky-bugs-cac90f72e77b

文章末尾有这样一段Go代码:

func main() {
    slice := make([]string, 1, 3)
  
    func(slice []string){
        slice = slice[1:3]
        slice[0] = "b"
        slice[1] = "b"
        fmt.Print(len(slice))
        fmt.Print(slice)
    }(slice)
    fmt.Print(len(slice)) 
    fmt.Print(slice)
}

我最初的猜测是会输出:

2 [b b]3 [ b b]

实际上输出的是:

2[b b]1[]

这表明,当匿名函数通过对传递给它的切片进行切片操作来创建一个新的局部切片时,会为切片分配一个新的底层数组。我通过以下修改后的代码进行了确认:

func main() {
	slice := make([]string, 1, 3)

	hdr := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Printf("main函数中底层数组的地址:%p\n", unsafe.Pointer(hdr.Data))

	func(slice []string) {
		hdr := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
		fmt.Printf("切片操作前函数中底层数组的地址:%p\n", unsafe.Pointer(hdr.Data))
		slice = slice[1:3]
		slice[0] = "b"
		slice[1] = "b"
		hdr = (*reflect.SliceHeader)(unsafe.Pointer(&slice))
		fmt.Printf("切片操作后函数中底层数组的地址:%p\n", unsafe.Pointer(hdr.Data))
		fmt.Print(len(slice))
		fmt.Println(slice)
	}(slice)
	fmt.Print(len(slice))
	fmt.Println(slice)
}

输出结果为:

main函数中底层数组的地址:0xc0000121b0
切片操作前函数中底层数组的地址:0xc0000121b0
切片操作后函数中底层数组的地址:0xc0000121c0
2[b b]
1[]

我的问题是:为什么匿名函数中的切片操作会导致分配一个新的数组?我理解的是,如果在main函数中创建了一个容量为3的切片,那么会创建一个长度为3的底层数组,当匿名函数操作切片时,它应该是在操作同一个底层数组。

英文:

I was reading this article which explains how slices in Go are implemented under the hood:
https://medium.com/swlh/golang-tips-why-pointers-to-slices-are-useful-and-how-ignoring-them-can-lead-to-tricky-bugs-cac90f72e77b

At the end of the article is this snippet of Go code:

func main() {
    slice:= make([]string, 1, 3)
  
    func(slice []string){
        slice=slice[1:3]
        slice[0]="b"
        slice[1]="b"
        fmt.Print(len(slice))
        fmt.Print(slice)
    }(slice)
    fmt.Print(len(slice)) 
    fmt.Print(slice)
}

My first guess was this would print:

2 [b b]3 [ b b]

In fact it prints:

2[b b]1[]

Which suggests that when anonymous function creates a new local slice by slicing the one passed to it as argument causes a new underlying array to be allocated for the slice. I've confirmed this with this modified version of code:

func main() {
	slice := make([]string, 1, 3)

	hdr := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Printf("adress of underlying array in main: %p\n", unsafe.Pointer(hdr.Data))

	func(slice []string) {
		hdr := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
		fmt.Printf("adress of underlying array in func before slicing: %p\n", unsafe.Pointer(hdr.Data))
		slice = slice[1:3]
		slice[0] = "b"
		slice[1] = "b"
		hdr = (*reflect.SliceHeader)(unsafe.Pointer(&slice))
		fmt.Printf("adress of underlying array in func after slicing: %p\n", unsafe.Pointer(hdr.Data))
		fmt.Print(len(slice))
		fmt.Println(slice)
	}(slice)
	fmt.Print(len(slice))
	fmt.Println(slice)
}

Which prints:

adress of underlying array in main: 0xc0000121b0
adress of underlying array in func before slicing: 0xc0000121b0
adress of underlying array in func after slicing: 0xc0000121c0
2[b b]
1[]

My question is: why is the slicing operation in anonymous function causing a new array to be allocated? My understanding was that if in main we are creating a slice with capacity 3, an underlying array with length 3 is created and when anonymous function is manipulating the slice, it is manipulating the same underlying array.

答案1

得分: 1

正如mkopriva提到的,重新切片不会重新分配任何内容。只有在追加新值超过切片的容量时才会进行重新分配(来源:https://go.dev/ref/spec#Appending_and_copying_slices)。

你得到的输出是因为你在切片中“能看到”的元素(包括打印时)取决于其长度(https://go.dev/ref/spec#Slice_types):

元素的数量被称为切片的长度,它永远不会是负数。[...] 切片s的长度可以通过内置函数len来获取;

使用slice := make([]string, 1, 3)构造的原始切片长度为1,所以当你打印它时,输出将是位于后备数组位置0一个元素,即空字符串。

通过以下代码:

slice = slice[1:3]
slice[0] = "b"
slice[1] = "b"

你实际上是在改变后备数组位置12上的元素,而这两个元素都不是原始切片的元素。

如果你将原始切片重新切片到容量上限-扩展其长度,它将打印出你期望的结果:

slice = slice[:cap(slice)]
fmt.Println(len(slice)) // 3
fmt.Println(slice)      // [ b b]
                        //   ^ 第一个是空字符串
英文:

As mkopriva mentioned, the reslicing does not reallocate anything. Reallocation happens only when appending new values would exceed the slice's capacity (source).

The output you got is because the elements you are "able to see" in a slice (including when you print it) depend on its length:

> The number of elements is called the length of the slice and is never negative. [...] The length of a slice s can be discovered by the built-in function len;

The original slice constructed with slice := make([]string, 1, 3), has length=1, so when you print it, the output will be the one element at position 0 of the backing array, which is an empty string.

With this code:

slice = slice[1:3]
slice[0] = "b"
slice[1] = "b"

you are effectively mutating the elements at position 1 and 2 of the backing array, none of which is an element of the original slice.

If you reslice the original slice up to capacity — thus extending its length, it will print what you expect:

	slice = slice[:cap(slice)]
	fmt.Println(len(slice)) // 3
	fmt.Println(slice)      // [ b b]
                            //  ^ first is empty string

huangapple
  • 本文由 发表于 2021年12月26日 20:52:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/70486595.html
匿名

发表评论

匿名网友

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

确定