4

In golang, slice and map are both reference types. When you simply need to modify elements in slice/map, the modification to slice/map members will be "broadcasted" to all slices. E.g, given m1 := make(map[int]int); m2 := m1, m1[3] = 5 will lead to m2[3] == 5.

However, when you try to add new elements into these two types, things start to differ. As is shown in the example below, the new elements added into a map param will be shown automatically in the argument; however, the new elements added into a slice, are 'discarded' in the argument.

Question is, why is this difference?

func editMap(m map[int]int) {
    m[3] = 8
    m[4] = 9
}

func editSlice(s []int) {
    s = append(s, 5)
    s = append(s, 9)
}

func main() {
    m := make(map[int]int, 0)
    m[1] = 5
    m[2] = 3
    fmt.Printf("%v\n", m)  //map[1:5 2:3]
    editMap(m)
    fmt.Printf("%v\n", m)  //map[1:5 2:3 3:8 4:9]

    s := make([]int, 2)
    s[0] = 2
    s[1] = 5
    fmt.Printf("%v\n", s)  //[2 5]
    editSlice(s)
    fmt.Printf("%v\n", s)  //[2 5]
}

Edit: I may not be clear on the intention of this question and please let me rephrase it(sorry for this and thanks for all the internal details).

What I really want to ask is, apparently map is implemented as a pointer to hide all details for the hash map; why wasn't slice implemented similarly?

The current slice implementation is indeed quite lightweight, however, from an API point of view (API between golang users like us and golang maintainers like Ross Cox), the APIs of these two 'reference' types are not that uniform and may lead to pitfalls for developers new to golang.

phye
  • 49
  • 1
  • 5
  • 2
    The most important thing to take away from this is that *there are no references in Go, everything is passed by value*. Take a look at [Go slice internals](https://blog.golang.org/go-slices-usage-and-internals) for an excellent run-down of how slices really work and why they behave as you've observed. – Adrian Dec 01 '17 at 14:43
  • Thanks Adrian. I knew the details behind golang slice internals, was just curious why slice was so designed (As edited above). – phye Dec 05 '17 at 02:42
  • I believe the question deserves more upvotes. While the answers indeed providing some insights about the implementation, the *reasoning* for choosing this particular way of implementation is not fully clear, at least for me. – John Snow Jan 10 '19 at 10:48

3 Answers3

9

The difference in behavior lies in the implementation of those types.

Maps are pointers to a data structure, while slices are small structs which contain the pointer to the backing array, the slice length and capacity. reflect.SliceHeader models a slice header:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

This is the main reason for the deviation: pointer vs struct.

When you add a new element to a map, the pointer remains the same. The pointed map structure may change, but the map pointer will not.

When you change an element of a slice, you effectively change the element of the backing array. The slice header will not change: it will continue to hold the same pointer, length and capacity. Any copy of the slice value (the slice header) points to the same backing array, and as such, all will see the changed element.

When you add a new element to a slice, the slice header that describes the new slice (which contains the additional element) must change: the length must be increased by 1, and optionally the pointer and capacity may change too (if a new backing array had to be allocated to accommodate the new element).

Everything is passed by value in Go. This is the 2nd reason for the deviation. When a slice is passed, a copy is made from the header, and if something is appended to the copy, even if the result is properly assigned back to it (back to the copy), the original slice header will know nothing about that.

When a map is passed, the map value (which is a pointer) will also be copied, but both the original map pointer and the copy map pointer will point to the same map structure. Adding a value or changing the map via any of those pointers will change the one and only pointed map structure.

To make them behave the same, you would have to make them the same "type" of type, that is: pointers. As noted above, maps are already (hidden) pointers. If you go ahead and start passing pointers to slices, and you operate on the pointed value, they will behave the same as maps. In practice this is rarely used (there is even less language support to aid working with slice pointers than array pointers), and rather the alternative approach is wide spread where the new slice is returned. You can read more about it here: Slicing a slice pointer passed as argument

icza
  • 389,944
  • 63
  • 907
  • 827
  • Your answer is overly verbose IMHO. In order to use a language and to get an idea about the underlying concept, it is not necessary to know every detail. Instead of repeating the implementation, explaining, why the implementation makes sense would have been more valuable. – metakeule Dec 01 '17 at 17:24
  • I totally understand the implementation detail hiding after slice(as stated in The Go Programming Language, i.e., in slice type we have Len, Cap .etc), and also totally understand the copy by value mechanism ---- the same mechanism with C programming language, the actual params passed are actually those params pushed to stack. The reason that I asked this question is actually not these implementation difference, but the logic behind the difference, i.e., why for map param, we have in place CRUD, but for slice, we have to return the slice if the slice passed in slice have been appended. – phye Dec 04 '17 at 02:47
0

To modify the slice you just need to edit the code like this (https://play.golang.org/p/2SeP93itIL):

func editSlice(s *[]int) {
    *s = append(*s, 5)
    *s = append(*s, 9)
}

There is some explanation in Effective Go:

If a function takes a slice argument, changes it makes to the elements of the slice will be visible to the caller

That's why modified arguments of the slice will be visible to the caller, but the new slice itself is not visible until you return it or assign to the existing variable. Because append returns a new slice:

We must return the slice afterwards because, although Append can modify the elements of slice, the slice itself (the run-time data structure holding the pointer, length, and capacity) is passed by value.

hsrv
  • 1,372
  • 10
  • 22
0

If two objects have the same memory address, then if you change the value of one of them, the other will change, because they are actually one object.

In your code, m in editMap and m in main have the same value: the address of a same dictionary object. However, the values of s in editSlice and s in main are two different objects.

I hope my explanation is clear enough.

Flying onion
  • 126
  • 7