Home > Software engineering >  A design question about Go's map and slice troubled me a lot
A design question about Go's map and slice troubled me a lot

Time:03-19

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.

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]
}

Look at the output, if slice is passed by 'reference', it won't be like [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 ...]
}

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...)

CodePudding user response:

This was also asked on the golang-nuts mailing list, with this as part of the reply from Brian Candler:

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.

CodePudding user response:

FYI : are-slices-passed-by-value

Everything in Go is passed by value, slices too. But a slice value is a header, describing a contiguous section of a backing array, and a slice value only contains a pointer to the array where the elements are actually stored. The slice value does not include its elements (unlike arrays).

So when you pass a slice to a function, a copy will be made from this header, including the pointer, which will point to the same backing array. Modifying the elements of the slice implies modifying the elements of the backing array, and so all slices which share the same backing array will "observe" the change.

To see what's in a slice header, check out the reflect.SliceHeader type:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

See related / possible duplicate question: Are Golang function parameter passed as copy-on-write?

Read blog post: Go Slices: usage and internals

  • Related