Why does Go forbid taking the address of (&) map member, yet allows (&) slice element?

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

Why does Go forbid taking the address of (&) map member, yet allows (&) slice element?

问题

Go不允许获取映射成员的地址:

// 如果我这样做:
p := &mm["abc"]
// 语法错误 - 无法获取 mm["abc"] 的地址

这样设计的原因是,如果允许获取地址,当映射的后端存储增长或缩小时,地址可能会变得无效,给用户带来困惑。

但是,当切片的容量不足时,Go会重新定位切片,然而,Go允许我们获取切片元素的地址:

a := make([]Test, 5)
a[0] = Test{1, "dsfds"}
a[1] = Test{2, "sdfd"}
a[2] = Test{3, "dsf"}

addr1 := reflect.ValueOf(&a[2]).Pointer()
fmt.Println("a[2]的地址:", addr1)

a = append(a, Test{4, "ssdf"})
addrx := reflect.ValueOf(&a[2]).Pointer()
fmt.Println("追加后a[2]的地址:", addrx)

// 注意,在追加后,第一个地址无效
a[2]的地址: 833358258224
追加后a[2]的地址: 833358266416

为什么Go设计成这样?获取切片元素的地址有什么特殊之处?

英文:

Go doesn't allow taking the address of a map member:

// if I do this:
p := &mm["abc"]
// Syntax Error - cannot take the address of mm["abc"]

The rationale is that if Go allows taking this address, when the map backstore grows or shinks, the address can become invalid, confusing the user.

But Go slice gets relocated when it outgrows its capacity, yet, Go allows us to take the address of a slice element:

 a := make([]Test, 5)
 a[0] = Test{1, "dsfds"}
 a[1] = Test{2, "sdfd"}
 a[2] = Test{3, "dsf"}

 addr1 := reflect.ValueOf(&a[2]).Pointer()
 fmt.Println("Address of a[2]: ", addr1)

 a = append(a, Test{4, "ssdf"})
 addrx := reflect.ValueOf(&a[2]).Pointer()
 fmt.Println("Address of a[2] After Append:", addrx)

 // Note after append, the first address is invalid
 Address of a[2]:  833358258224
 Address of a[2] After Append: 833358266416

Why is Go designed like this? What is special about taking address of slice element?

答案1

得分: 43

切片(slices)和映射(maps)之间有一个重要的区别:切片是由一个后备数组支持的,而映射则不是。

如果一个映射增长或缩小,指向映射元素的潜在指针可能会变成一个指向无效内存的悬空指针(未初始化的内存)。这里的问题不是“用户的困惑”,而是它会破坏Go语言的一个重要设计元素:没有悬空指针。

如果一个切片的容量不够了,就会创建一个新的、更大的后备数组,并将旧的后备数组复制到新的数组中;而旧的后备数组仍然存在。因此,从“未扩展”的切片中获取的指向旧的后备数组的指针仍然是指向有效内存的有效指针。

如果你有一个仍然指向旧的后备数组的切片(例如,因为你在将切片扩展到容量之外之前复制了切片),你仍然可以访问旧的后备数组。这与切片元素的指针关系较小,而与切片作为数组的视图以及在切片增长期间复制数组有关。

请注意,在切片缩小期间没有“减少切片的后备数组”的操作。

英文:

There is a major difference between slices and maps: Slices are backed by a backing array and maps are not.

If a map grows or shrinks a potential pointer to a map element may become a dangling pointer pointing into nowhere (uninitialised memory). The problem here is not "confusion of the user" but that it would break a major design element of Go: No dangling pointers.

If a slice runs out of capacity a new, larger backing array is created and the old backing array is copied into the new; and the old backing array remains existing. Thus any pointers obtained from the "ungrown" slice pointing into the old backing array are still valid pointers to valid memory.

If you have a slice still pointing to the old backing array (e.g. because you made a copy of the slice before growing the slice beyond its capacity) you still access the old backing array. This has less to do with pointers of slice elements, but slices being views into arrays and the arrays being copied during slice growth.

Note that there is no "reducing the backing array of a slice" during slice shrinkage.

答案2

得分: 7

map和slice之间的一个基本区别是,map是一个动态数据结构,随着其包含的值的增长而移动。Go语言中map的具体实现甚至可以在插入和删除操作期间逐渐增长,直到所有值都被移动到更大的内存结构中。因此,你可能会删除一个值,然后突然另一个值会移动。而slice只是一个指向子数组的接口/指针。slice不会增长。append函数可以将一个slice复制到另一个具有更大容量的slice中,但它会保留旧的slice,并且它本身是一个函数,而不仅仅是一个索引操作符。

用map实现者自己的话来说:

https://www.youtube.com/watch?v=Tl7mi9QmLns&feature=youtu.be&t=21m45s
"它会干扰这个增长过程,所以如果我取得某个桶中的某个条目的地址,然后我把那个条目保存很长时间,与此同时map增长了,那么突然间那个指针指向的是一个旧的桶而不是一个新的桶,那个指针现在就是无效的,所以很难在不限制增长方式的情况下提供获取map中值的地址的能力...C++的增长方式不同,所以你可以获取一个桶的地址"

因此,即使允许使用&m[x]可能是可以的,并且对于短暂的操作会很有用(对值进行修改,然后不再使用该指针),实际上map在内部也是这样做的,但我认为语言设计者/实现者选择了对map保持谨慎,不允许使用&m[x],以避免程序中可能会保留指针很长时间而没有意识到它将指向与程序员所想的不同的数据的微妙错误。

另请参见https://stackoverflow.com/questions/49202077/why-doesnt-go-allow-taking-the-address-of-map-value中的相关评论。

英文:

A fundamental difference between map and slice is that a map is a dynamic data structure that moves the values that it contains as it grows. The specific implementation of Go map may even grow incrementally, a little bit during insert and delete operations until all values are moved to a bigger memory structure. So you may delete a value and suddenly another value may move. A slice on the other hand is just an interface/pointer to a subarray. A slice never grows. The append function may copy a slice into another slice with more capacity, but it leaves the old slice intact and is also a function instead of just an indexing operator.

In the words of the map implementor himself:

> https://www.youtube.com/watch?v=Tl7mi9QmLns&feature=youtu.be&t=21m45s
> "It interferes with this growing procedure, so if I take the address
> of some entry in the bucket, and then I keep that entry around for a
> long time and in the meantime the map grows, then all of a sudden that
> pointer points to an old bucket and not a new bucket and that pointer
> is now invalid, so it's hard to provide the ability to take the
> address of a value in a map, without constraining how grow works...
> C++ grows in a different way, so you can take the address of a bucket"

So, even though &m[x] could have been allowed and would be useful for short-lived operations (do a modification to the value and then not use that pointer again), and in fact the map internally does that, I think the language designers/implementors chose to be on the safe side with map, not allowing &m[x] in order to avoid subtle bugs with programs that might keep the pointer for a long time without realizing then it would point to different data than the programmer thought.

See also https://stackoverflow.com/questions/49202077/why-doesnt-go-allow-taking-the-address-of-map-value for related comments.

答案3

得分: -1

我已经阅读了很多关于数组指针和映射指针之间区别的解释,但仍然感觉有些奇怪。

考虑这个例子:https://go.dev/play/p/uzADxzdq2EP

我可以获取指向数组的第一个对象的指针,但是在我向数组中添加另一个对象之后,原始指针仍然存在,但它不再指向当前数组的第一个对象。它指向原始值。当然,它不是指向一个空对象,它指向同一个对象,但对于某个版本的正确来说,它不再是“正确”的。

我不确定我在这里的观点是什么,除了它只是...奇怪。

英文:

I've read a bunch of explanations about the difference between array pointers and map pointers and it all still seems a tad odd.

Consider this: https://go.dev/play/p/uzADxzdq2EP

I can get a pointer to the zeroth array object but after I add another object to the array the original pointer is still there but it no longer points to the zeroth object of the current array. It points to the original value. Sure, it's not pointing to a nil object, it's pointing to the same object, but it's no longer 'correct' for some version of correct.

I'm not sure what my point is here other than it's just...odd.

huangapple
  • 本文由 发表于 2015年9月10日 15:12:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/32495402.html
匿名

发表评论

匿名网友

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

确定