关于 Go 的地图和切片的设计问题让我很困扰

A design question about Go's map and slice troubled me a lot

提问人:qizong007 提问时间:3/17/2022 更新时间:3/19/2022 访问量:576

问:

为什么 Go 的切片有“复制陷阱”,而 map 没有?

假设我们有一个函数,它以 slice 作为输入参数,如果 slice 在函数中展开,则只更改复制的切片结构,而不是原来的切片结构。原始切片仍指向原始数组,函数中的切片由于扩展而更改了数组指针。

func main() {
    a := []int{1, 2}
    fmt.Println(a) // [1 2]
    doslice(a)
    fmt.Println(a) // [1 2]
}

func doslice(a []int) {
    a = append(a, 3, 4, 5, 6, 7, 8, 9)
    a[0] = 200
    fmt.Println(a) // [200 2 3 4 5 6 7 8 9]
}

看看输出,如果切片是通过“引用”传递的,它不会像 [1 2] 那样。

func main() {
    mp := map[int]int{1:1,2:2}
    domap(mp)
    fmt.Println(mp) // map[1:1 2:2 3:3 4:4 ...], the same as below
}

func domap(mp map[int]int) {
    for i := 3; i < 100; i++ {
        mp[i] = i
    }
    fmt.Println(mp) // map[1:1 2:2 3:3 4:4 ...]
}

看看这个,我们在 domap() 中所做的操作奏效了! 是陷阱吗?! 我的意思是,map由'reference'(*hmap)传递,slice由'value'(SliceHeader)传递。 为什么地图和切片被设计为所有内部引用类型,为什么它如此不一致? 也许这只是一个设计问题,但为什么呢?为什么要这样切片和映射?

以下是我对证明为什么只能通过“引用”传递的猜测:map

Assume that map is passed by value -> hmap struct type

(1) After Init: (hmap outside the function)
hmap.buckets = bucketA
hmap.oldbuckets = nil

(2) After passing the param, entering the function: (hmap inside the function)
hmap.buckets = bucketA
hmap.oldbuckets = nil

(3) After triggering the expanding: (hmap inside the function)
hmap.buckets = bucketB
hmap.oldbuckets = bucketA

不一样slice

There is no incremental migration, 
and there is no oldbuckets, 
so you can use a structure because the function is isolated from the outside, 
whereas map is not, and oldbuckets are referenced outside the function.

我的意思是,这个设计的最初目的可能是传递值,以防止函数中原始变量的直接修改,但映射不能通过值传递。一旦该值被传递到地图,原始地图数据将在函数扩展过程中丢失。

如果有人能帮我解决这个问题,我将不胜感激。多谢!

(我为我可怜的Chinglish感到难过,我希望我描述得很好......:(这真的让我很困扰......

Go 切片 引用传递 按 值传递 GO-MAP

评论

2赞 icza 3/17/2022
设计问题不太适合 SO。它们通常是基于意见的,应该针对语言作者。
2赞 icza 3/17/2022
请记住,地图实际上是指针,而切片是包含指针的结构。了解为什么切片值有时会过时,但永远不会映射值?这将使人们很容易记住他们(应该)的行为方式。

答:

0赞 thepudds 3/19/2022 #1

在golang-nuts邮件列表上也提出了这个问题,这是Brian Candler回复的一部分:

我想你已经很好地理解了这个问题。从源代码 https://golang.org/src/runtime/slice.go

类型 slice struct { 阵列不安全。指针 len int cap int }

正如你所发现的,这是按值传递的。你当然可以通过一个 显式指向此类值的指针(如果选择)。

是陷阱吗?!

嗯,这是你必须学习的语言,但我认为 这是可用设计选项中的最佳选择。

字符串和切片彼此一致:它们是普通的 structs,包含指向数据、长度和(用于切片)的指针 能力。

是否可以已实现切片和字符串,以便它们的值为 总是指向结构的指针?我想是的,但我想你会的 最终仍然在更深层次上遇到类似的问题。复制时 一个切片 (b := a),或者将其作为函数参数传递,那么你就会 复制指针,这样同一切片将有两个别名,并且 对一个的修改对另一个是可见的。但是当你 sub-slice (b := a[1:2]),则您将被迫分配一个新切片 结构。因此,切片的行为将取决于 确切地说,它是如何生成的,因此突变一个切片可能会也可能不会影响其他切片。我认为总体上会更令人困惑。

0赞 tianwei 3/19/2022 #2

仅供参考:are-slices-passed-by-value

Go 中的所有内容都是按值传递的,切片也是如此。但切片值是 一个标头,描述后备数组的连续部分,以及一个 slice 值仅包含指向数组的指针,其中元素 实际存储。切片值不包括其元素 (与数组不同)。

因此,当您将切片传递给函数时,将从中复制 标头,包括指针,它将指向相同的后背 数组。修改切片的元素意味着修改 后备数组的元素,以及共享相同元素的所有切片 后备数组将“观察”更改。

若要查看切片标头中的内容,请查看以下类型:reflect.SliceHeader

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

请参阅相关/可能的重复问题:Golang 函数是 参数作为写入时复制传递?

阅读博客文章:Go Slices:用法和内部结构