0

Can someone explain to me why the output of r.a is empty as I have added a value of 2 into the list?

package main

import (
    "fmt"
)

func main() {
    var r R
    r.b = make(map[int]int)

    r.add()
    fmt.Println(r) // outputs {[] map[2:2]}
}

type R struct {
    a []int
    b map[int]int
}

func (r R) add()  {
    r.a = append(r.a, 2)
    r.b[2] = 2
}
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
zhuang
  • 11
  • 4
  • 1
    "Value receiver" means `add` receives _a copy of the contents_ of r, not a pointer to r. Internal to `add`, `r.a` is replaced with an updated slice value that points to different memory that includes the new `2`, but nothing changes in `main`'s copy of `r`. When slice operations like this _do_ work, like when `Read` saves data into a byte slice, it's because the two copies of a slice already pointed to the same memory, and the slice-modifying operation just changed that already-shared memory. This introduction to how slices work might help: https://blog.golang.org/slices – twotwotwo Nov 27 '18 at 21:48
  • For other related reading, Russ Cox wrote [a post about how slices work](https://research.swtch.com/godata) you may find helpful and I answered [a question about pointers and values](https://stackoverflow.com/questions/23542989/pointers-vs-values-in-parameters-and-return-values/23551970). – twotwotwo Nov 27 '18 at 21:51
  • 1
    @twotwotwo Thanks very much. This introduction of slice is really awesome [blog.golang.org/slices](https://blog.golang.org/slices). The concept of sliceHeader makes things clear. – zhuang Nov 27 '18 at 23:03
  • Great, glad I could help! – twotwotwo Nov 27 '18 at 23:20

1 Answers1

1

A short excerpt from the Tour of Go states that:

Methods with pointer receivers can modify the value to which the receiver points [...]. Since methods often need to modify their receiver, pointer receivers are more common than value receivers.

Why is r.b shown correctly while r.a is not modified at all?

As already stated in my answer below, your add() method is a value receiver. Therefore, it will take your initialized struct (i.e. r), copy it and then modify it accordingly. Because you have initialized a new map under r.b in your main() function, here only the reference to this map is copied but not the whole map. Therefore, the manipulation on the map works but not on the slice r.a. But why does r.a not change at all? That's because append(), which is located in the add() method, stores a new slice header under your a property, and is pointing to a different section of the underlying array. At the end, your value receiver method add() made a copy of r, set a new slice header under the property a, and never changed the original struct r, that has been defined in the main() function as it was copied through the value receiver method add().

In your case the add() method is a so called value receiver method that cannot do any manipulation on your defined struct r located in the main() function directly, but copies it and does the manipulation afterwards. Therefore, you need to turn your add() method into a pointer receiver method just like this:

func (r *R) add()  {
    r.a = append(r.a, 2)
    r.b[2] = 2
}

Now the method is taking the actual reference of your struct r, that is initiated in the main() function, and modifies it accordingly.

How could it work without changing your add() method to a pointer receiver?

You simply would have to return the copied struct in your add() method like this:

package main

import (
    "fmt"
)

func main() {
    var r R
    r.b = make(map[int]int)
    fmt.Println(r.add()) // outputs {[2] map[2:2]}
}

type R struct {
    a []int
    b map[int]int
}

func (r R) add() R {
    r.a = append(r.a, 2)
    r.b[2] = 2
    return r
}
bajro
  • 1,199
  • 3
  • 20
  • 33
  • Thanks for the detailed answer. My key point is that why an update to `b` (which is a map) can be reflected in the output, while an update to `a` (which is a slice) is ignored. I think `r.b` is a pointer to the underlying map data, and `r.a` is also a pointer to the underlying slice data. Please correct me. – zhuang Nov 27 '18 at 21:57
  • According to the golang language specification, a value receiver method operates on a copy of the original struct. In your case, the `add()` method copies your struct `r`, appends `2` to `r.a` and also adds `2` to the key `2` in the map of `r.b`. The key here is, that the reference (i.e. pointer) of the initialized map is copied to the newly created struct as your `add()` method is a value receiver. So at the end, after calling `add()` on `r`, it creates a new struct (by copying the old one) but copies the reference of the initialized map with it. Therefore, your code behaves like that. – bajro Nov 27 '18 at 22:16
  • @zhuang you are right that a slice is a reference type like a map but append to a slice can create a new slice with a different underlying array. This is so that slices can be contiguous in memory. Inserting into a map does not create a new map. – Chris Drew Nov 27 '18 at 22:23
  • I have edited my answer to give an example on how to do it with a pointer receiver and also with a value receiver. Personally I would go for the pointer receiver as it just modifies the original struct but does not copy it. – bajro Nov 27 '18 at 22:24
  • @Bajro @Chris Drew Thanks very much. I think I understand what has happened right now. In function `add()`, as your gays said, `r` is indeed a copy of struct `R` defined in the `main()` function. `r.a` is a copy of the sliceHeader, looking like this, `sliceHeader{Length: 0, Capacity: 10, ZerothElement: &iBuffer[0]}`. – zhuang Nov 27 '18 at 23:27
  • So when we do `r.a[0] = 1`, the change will be reflected in the origin one because they share the same underlying array and the `r.a` in the `main()` function also points to the value in location 0. When we do `r.a = append(r.a, 2)`, r.a becomes a new sliceHeader. The underlying array may changes at the same time. But nothing happens to the original r.a in `main()` function, because the sliceHeader of r.a in `main()` function stays just the same. – zhuang Nov 27 '18 at 23:27
  • @zhuang Yes exactly thats the point. When using slices, and in this case with the `append()` method, you are just passing the header (i.e. sliceHeader) and it just points to the new slice of the original slice. So the underlying slice is still there, but the newly created slice with `append()` is pointing to a new header of the slice. Here is another link to a blog post explaining slices very well https://blog.golang.org/go-slices-usage-and-internals – bajro Nov 28 '18 at 08:21