为什么 Golang 的切片(slices)内部设计成这样?

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

Why golang slices internal designed like this?

问题

代码:

func main() {

    a := []int{1, 2}
    printSlice("a", a)

    b := a[0:1]
    printSlice("b origin", b)

    b = append(b, 9)
    printSlice("b after append b without growing capacity", b)
    printSlice("a after append b without growing capacity", a)

    b = append(b, 5, 7, 8)
    printSlice("a after append b with grown capacity", a)
    printSlice("b after append b with grown capacity", b)
    
    b[0] = 1000
    printSlice("b", b)
    printSlice("a", a)      

}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

输出:

a len=2 cap=2 [1 2]
b origin len=1 cap=2 [1]
b after append b without growing capacity len=2 cap=2 [1 9]
a after append b without growing capacity len=2 cap=2 [1 9]
a after append b with grown capacity len=2 cap=2 [1 9]
b after append b with grown capacity len=5 cap=6 [1 9 5 7 8]
b len=5 cap=6 [1000 9 5 7 8]
a len=2 cap=2 [1 9]

有趣的是最后两行的输出。我已经知道切片只是底层数组的一个窗口。当在其容量内对其进行重新切片时,两个切片共享相同的底层数组,但是当我将其重新切片以超出其容量时,两个切片具有不同的底层数组。但是为什么Go语言的设计者选择不将原始切片的底层数组更改为新切片的底层数组,以使两个切片仍然具有相同的底层数组呢?在当前状态下,当我更改新切片的某些元素的值时,我必须检查是否更改了底层数组,以决定此操作是否对其他由它支持的切片产生副作用(请参见输出的最后两行)。我认为这很尴尬。

英文:

Code:

func main() {

    a := []int{1, 2}
    printSlice("a", a)

    b := a[0:1]
    printSlice("b origin", b)

    b = append(b, 9)
    printSlice("b after append b without growing capacity", b)
    printSlice("a after append b without growing capacity", a)

    b = append(b, 5, 7, 8)
    printSlice("a after append b with grown capacity", a)
    printSlice("b after append b with grown capacity", b)
    
    b[0] = 1000
    printSlice("b", b)
    printSlice("a", a)      

}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
	    s, len(x), cap(x), x)
}

Output:

a len=2 cap=2 [1 2]
b origin len=1 cap=2 [1]
b after append b without growing capacity len=2 cap=2 [1 9]
a after append b without growing capacity len=2 cap=2 [1 9]
a after append b with grown capacity len=2 cap=2 [1 9]
b after append b with grown capacity len=5 cap=6 [1 9 5 7 8]
b len=5 cap=6 [1000 9 5 7 8]
a len=2 cap=2 [1 9]

The interesting thing is at the last two printed lines. I already know that a slice is just a window of underlying array. When reslicing it within is capacity, then the two slices share the same underlying array, but When I reslice it to grow beyond its capaccity, the two slices have distinct underlying array. But why golang designers choose not to change the underlying array of the origin slice to the underlying array of the new slice, so as to make both slices still have the same underlying array? In current state when I changed the value of some elements of newly resliced slice I have to check if I changed the underlying array to decide if this operation have side effects on other slices backed up by it(see the last two lines of Output). I think it's awkward.

答案1

得分: 3

但是为什么golang的设计者选择不改变原始切片的底层数组为新切片的底层数组,以使得两个切片仍然具有相同的底层数组呢?

主要原因是,同一数组的切片可以出现在程序的任何地方,完全不同的函数、包等。鉴于切片在内存中的布局方式,Go语言需要“查找”所有共享该数组的切片来更新它们,但它没有这样的方式。

其他一些数组列表实现的方法(比如Python的列表)是,你传递的实际上是一个指向类似Go切片的指针,如果两个变量持有“相同的列表”,使用一个变量进行追加操作时,另一个变量也会显示出相同的结果。这也会带来一些效率上的开销,需要进行另一个指针查找来获取a[0]。在那些确实需要在这里进行追加操作以作为在那里进行追加操作的情况下,可以使用切片的指针。

切片的指针可以提供别名,但不提供子切片功能。要获取所有你所请求的内容,你需要一种不同的排列方式,我无法想到一个现实中的例子(偏移量、长度和指向struct { capacity int; firstElem *type }的指针)。

英文:

> But why golang designers choose not to change the underlying array of the origin slice to the underlying array of the new slice, so as to make both slices still have the same underlying array?

Mainly, slices of the same array can appear absolutely anywhere in the program--completely different functions, packages, and so on. Given how slices are laid out in memory, Go would have to "find" all slices sharing the array to update them; it has no way to.

The approach of some other array-list implementations (like Python lists) is that what you pass around is really a pointer to something like a Go slice, and if two variables hold "the same list", an append using one variable will also show up when you look at the other. That also has some efficiency cost--another pointer lookup to do a[0]. In those circumstances where you really need an append over here to act as an append over there, you can use pointers to slices.

Pointers to slices give you aliasing if you want it but don't provide subslicing--to get everything you ask for, you'd need a different arrangement that I can't think of an example of from in the wild (offset, length, and pointer to struct { capacity int; firstElem *type }).

huangapple
  • 本文由 发表于 2015年1月10日 10:47:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/27872365.html
匿名

发表评论

匿名网友

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

确定