信的内容是我 22 年问 Google Golang Nuts 的问题以及得到的回复,原信链接:https://groups.google.com/g/golang-nuts/c/Q7_Dj_ogVJE/m/_5TSXCIaBgAJ?pli=1
信件内容
为什么Go的slice有“复制陷阱”,而map却没有?
假设我们有一个以 slice 作为输入参数的函数,如果在函数中展开 slice,则只会改变复制的 slice 结构,而不是原来的 slice 结构。原来的slice仍然指向原来的数组,而函数中的slice因为扩容而改变了数组指针。
看一下输出,如果slice是通过“引用”传递的,它就不会是[1 2]。再看这个例子:
我们在 domap()
中完成的操作成功了!
这难道是陷阱吗?!
我的意思是,map是通过“引用”(*hmap
) 传递的,slice是通过“值”(SliceHeader
) 传递的。
为什么map和slice被设计成都是内部引用类型,为什么这么不一致呢?
也许这只是一个设计问题,但为什么呢?为什么要这样slice和map?
以下是我对证明为什么map只能通过“引用”传递的猜测:
假设map是按值传递的-> hmap结构类型
(1)init之后:(函数外的hmap)
hmap.buckets = bucketA
hmap.oldbuckets = nil
(2)传递param后,进入函数:(函数内的hmap)
hmap.buckets = bucketA
hmap.oldbuckets = nil
(3)触发扩容后:(函数内的hmap)
hmap.buckets = bucketB
hmap.oldbuckets = bucketA
但slice不一样!
没有增量迁移,也没有oldbuckets, 所以可以使用结构体,因为函数与外部隔离,而map则不然,oldbuckets是在函数外部引用的。
我的意思是,这样设计的最初目的可能是传值,防止直接修改函数中的原始变量,但是map不能传值。一旦将值传递给map,在函数内执行扩容的过程中原始map的数据就会丢失。
如果有人能帮助我解决这个问题,我将不胜感激。多谢!
(我为我垃圾的中式英语感到抱歉,我希望我把问题说清楚了…😢这真的让我很困扰…)
golang-nuts的回信
已经翻译成中文了
@Brian Candler
(顺便说一句:请不要发布屏幕截图。纯文本更易于阅读,并且可以复制粘贴。您也可以使用 https://go.dev/play/ 粘贴代码片段)
我想你已经很好地理解了这个问题。来自 https://golang.org/src/runtime/slice.go 的源代码:
type slice struct {array unsafe.Pointerlen intcap int
}
正如您所发现的,这是按值传递的。如果你愿意,当然可以显式传递指向此类值的指针。
这是陷阱吗?!
嗯,这是你必须学习的语言知识,但我认为这是可用设计选项中的最佳选择。
string 和 slice 彼此一致:它们是普通结构,包含指向数据的指针、长度和容量(对于slice)。
是否可以实现值始终是指向结构的“指针”的 slice 和 string ?
我想应该是可以的,但我认为在更深层次上你仍然会遇到类似的问题。
当复制 slice (b := a
) 或将其作为函数参数传递时,这将会复制一个指针,因此同一个 slice 将有两个别名,并且对一个slice的修改将对另一个也可见。但是,当进行子切片 (b := a[1:2]
) 时,将被迫分配一个新的slice结构。
因此,slice 的行为将完全取决于它的生成方式,因此改变slice可能会也可能不会影响其他slice。我认为整体上会更加混乱。
最重要的是,你仍然会遇到与今天相同的问题:
两个slice可能会也可能不会共享相同的底层缓冲区。
map和channel彼此一致:它们都有比较大的数据结构,并且值是指向该结构的指针。我认为将map值设置为指针是有充分理由的;这意味着你可以做一些方便的事情,比如:
m["foo"] = "bar"
而不是:
m = m.set("foo", "bar") // compare: s = append(s, "foo")
但另一方面,map或channel的零值并不是很有用:它是一个 nil 指针,它告诉您有零个元素,但不能添加任何元素。这使得它们在构建包含这些内容的结构时不方便,因为您需要显式地 make(…) 。这是 Go 新手抱怨的另一件事。