A question troubled me for a long time (about slice and map)

175 views
Skip to first unread message

王帅真

unread,
Mar 17, 2022, 9:47:17 AM3/17/22
to golang-nuts
Why does Go's slice has 'replication trap', but map does not? 
Suppose we have a function that takes slice as an input parameter, and if slice is expanded in the function, only the copied slice structure is changed, instead of the original slice structure. The original slice still points to the original array, and the slice in the function has changed the array pointer because of the expansion. 
111.png
Look at the output, if slice is passed by 'reference', it won't be like [1 2].
222.png
Look at this, the operation we've done in domap() worked!
Is it a trap?!
I mean, map is passed by 'reference'(*hmap) and slice is passed by 'value'(SliceHeader). 
Why is it that map and slice are designed to be all inner referenced types and why is it so inconsistent?
Maybe it's just a design question, but why? Why slice and map like that?
----------------------------------------------------------------------------------------------------------------------------------------------
Below is my guess about proving why map can only be passed by 'reference':

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

But slice is different!

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.

I mean, the original purpose of this design may be passing value, to prevent direct modification of the original variable in the function, but map cannot be passed by value. Once the value is passed to the map, the original map data will be lost during the expansion of the function.
----------------------------------------------------------------------------------------------------------------------------------------------
I will appreciate that if some one can help me solve this problem. Thanks a lot!
(I feel sorry for my poor Chinglish, I hope I described this well...:( It really troubled me a lot...)

Brian Candler

unread,
Mar 17, 2022, 11:09:44 AM3/17/22
to golang-nuts
(Just as an aside: please don't post screenshot images.  Plaintext is easier to read and can be copy-pasted.  You can also use https://go.dev/play/ to paste code snippets)

I think you have understood the issue well. From the source code at https://golang.org/src/runtime/slice.go :

type slice struct {
        array unsafe.Pointer
        len   int
        cap   int
}

As you have found, this is passed by value.  You can of course pass a pointer to such a value explicitly, if you choose.

> Is it a trap?!

Well, it's something you have to learn about the language, but I think it's the best choice out of the design options which were available.

Strings and slices are consistent with each other: they are plain structs, containing pointer to data, length, and (for slices) capacity.

Could slices and strings have been implemented so that their value is always a *pointer* to a structure? I guess so, but I think you would still end up with a similar problem at a deeper level.  When you copy a slice (b := a), or pass it as a function argument, then you'd be copying a pointer, so you'd have two aliases to the same slice, and modifications to one would be visible to the other. But when you sub-slice (b := a[1:2]) then you'd be forced to allocate a new slice structure.  The behaviour of a slice therefore would be dependent on exactly how it was generated, and so mutating a slice *may or may not* affect other slices. I think would be more confusing overall.

On top of that, you'd still have the same problem which you have today: two slices *may or may not* share the same underlying buffer.

Maps and channels are consistent with each other: they have a large data structure, and the value is a pointer to that structure.  I think there is a very good reason for making map values be pointers; it means you can do convenient things like
    m["foo"] = "bar"
instead of
    m = m.set("foo", "bar")    // compare: s = append(s, "foo")

But on the flip side, the zero value of a map or channel is not very useful: it's a nil pointer, which tells you that have zero elements, but you cannot add any elements.  This makes them inconvenient when building a structure which contains these things, because you need to make(...) the values explicitly.  This is something else that newcomers to Go have been known to complain about.

Reply all
Reply to author
Forward
0 new messages