125

New to Go. Encountered this error and have had no luck finding the cause or the rationale for it:

If I create a struct, I can obviously assign and re-assign the values no problem:

type Person struct {
 name string
 age int
}

func main() {
  x := Person{"Andy Capp", 98}
  x.age = 99
  fmt.Printf("age: %d\n", x.age)
}

but if the struct is one value in a map:

type Person struct {
     name string
     age int
 }

type People map[string]Person

func main() {
  p := make(People)
  p["HM"] = Person{"Hank McNamara", 39}
  p["HM"].age = p["HM"].age + 1
  fmt.Printf("age: %d\n", p["HM"].age)
}

I get cannot assign to p["HM"].age. That's it, no other info. http://play.golang.org/p/VRlSItd4eP

I found a way around this - creating an incrementAge func on Person, which can be called and the result assigned to the map key, eg p["HM"] = p["HM"].incrementAge().

But, my question is, what is the reason for this "cannot assign" error, and why shouldn't I be allowed to assign the struct value directly?

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
sbeam
  • 4,622
  • 6
  • 33
  • 43
  • 1
    `p["HM"]` isn't quite a regular pointer value, because the values in a map get moved around in memory, and the old locations become invalid, when the map grows. So you can't do all the operations on it that you could do on a regular pointer. Besides your solution (change it to an assignment, one of the allowed operations, which seems good here), another approach (maybe good for large objects?) is to make the map value a regular old pointer that you *can* modify the underlying object through: http://play.golang.org/p/n5C4CsKOAV – twotwotwo Sep 24 '15 at 00:44
  • 2
    The specific things you can do with `p["HM"]` are scattered around [the spec](https://golang.org/ref/spec) if you search for "index expression": you can read the value, assign a new whole value, delete, increment/decrement numeric values. – twotwotwo Sep 24 '15 at 00:49
  • ah that makes sense - I did try making a pointer `*Person` at one point but I think I forgot to create the reference with `&` - still getting used to this. thanks, make it an answer and I'd accept... – sbeam Sep 24 '15 at 00:53
  • 1
    ^^ that (and other answers) was found with a trivial and obvious `[go] map cannot assign` search. – Dave C Sep 24 '15 at 01:44
  • 1
    you are right, I did a search but not the right one. I am ashamed :( but I think the takeaway is also that the "cannot assign" error means "the left side of the assignment is not an addressable value" (which imo would be a better and more searchable message) – sbeam Sep 24 '15 at 02:57

2 Answers2

183

p["HM"] isn't quite a regular addressable value: hashmaps can grow at runtime, and then their values get moved around in memory, and the old locations become outdated. If values in maps were treated as regular addressable values, those internals of the map implementation would get exposed.

So, instead, p["HM"] is a slightly different thing called a "map index expression" in the spec; if you search the spec for the phrase "index expression" you'll see you can do certain things with them, like read them, assign to them, and use them in increment/decrement expressions (for numeric types). But you can't do everything. They could have chosen to implement more special cases than they did, but I'm guessing they didn't just to keep things simple.

Your approach seems good here--you change it to a regular assignment, one of the specifically-allowed operations. Another approach (maybe good for larger structs you want to avoid copying around?) is to make the map value a regular old pointer that you can modify the underlying object through:

package main

import "fmt"

type Person struct {
    name string
    age  int
}

type People map[string]*Person

func main() {
    p := make(People)
    p["HM"] = &Person{"Hank McNamara", 39}
    p["HM"].age += 1
    fmt.Printf("age: %d\n", p["HM"].age)
}
twotwotwo
  • 28,310
  • 8
  • 69
  • 56
  • 15
    The suggestion to use a map of pointers, e.g. `map[string]*MyStruct` solved a similar problem for me. – Mike Ellis Sep 04 '18 at 00:08
  • Then why unordered_map in stl C++ can support addressing, which faces the same reallocation problem? – hunter_tech Dec 01 '20 at 12:53
  • @hunter_tech Just different hash map designs. I'm not an STL expert, but could be, for example, that the underlying array that's resized only stores _pointers_ to values (or to some larger piece--key/val pair, bucket, etc.). So growing that array means shuffling around the pointers but not the values. That'd be a little like when you store pointers to values in a Go hashmap, but done implicitly. Other C++ hashmaps, like [Abseil's `flat_hash_map`](https://abseil.io/docs/cpp/guides/container), do store values in the main array and invalidate iterators when it's grown. – twotwotwo Dec 01 '20 at 18:15
  • @hunter_tech You might prefer the Go/flat_hash_map approach to avoid having to follow a pointer (and perhaps wait for a cache miss) when reading items, if you don't care about the items moving. You might prefer the unordered_map approach if you need values to not move. Note that Go's approach where items move but that can't create dangling pointers (because the language bans taking the address of an item) wasn't an option in C++ (where you can take the address of most anything). So the designers might have thought stable item addresses would make unordered_map easier to use safely. – twotwotwo Dec 01 '20 at 18:44
  • @twotwotwo. 3ks. When we are referring the map, the value returned is returned "returned by value", that's the key difference of golang map design compared to stl. – hunter_tech Dec 02 '20 at 06:17
5

The left side of the assignment must b "addressable".

https://golang.org/ref/spec#Assignments

Each left-hand side operand must be addressable, a map index expression, or (for = assignments only) the blank identifier.

and https://golang.org/ref/spec#Address_operators

The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array.

as @twotwotwo's comment, p["HM"] is not addressable. but, there is no such definition show what is "addressable struct operand" in the spec. I think they should add some description for it.

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
Jiang YD
  • 3,205
  • 1
  • 14
  • 20