Wandering Thoughts archives

2021-07-23

Why it matters that map values are unaddressable in Go

A while ago, I wrote Addressable values in Go (and unaddressable ones too) as an attempt to get straight this tricky concept in Go, which I hadn't fully understood. To refresh, the Go specification's core description of this is covered in Address operators:

For an operand x of type T, the address operation &x generates a pointer of type *T to x. 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 an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal. [...]

One of the things that are explicitly not addressable are values in a map. As I mentioned in the original entry, the following is an error:

&m["key"]

On the surface this looks relatively unimportant. There aren't many situations where you might naturally explicitly take the address of a map value. But there turns out to be an important consequence of this, brought to my attention recently by this article.

One important thing in Go that addressability affects is Assignments:

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

Suppose that you have map values that are structs with fields. Because map values are not addressable and field selectors can only be applied to addressable struct operands, you cannot directly assign values to the fields of map values. The following is an error:

m["key"].field = 10

This will give you the clear error of 'cannot assign to struct field m["key"].field in map'. To make this work, you must assign the map value to temporary variable, modify the temporary, and put it back in the map:

t := m["key"]
t.field = 10
m["key"] = t

One reason I can think of for this restriction is that otherwise, Go might be required to silently materialize struct values in maps as a consequence of what looks like a simple field assignment. Consider:

m["nosuchkey"].field = 10

If this was to work, it would have to have the side effect of creating an entire m["nosuchkey"] value and setting it in the map for the key. Instead Go refuses to allow it, at compile time.

In the usual way of addressable values in Go, this will work if the map values are pointers to structs and the syntax is exactly the same. This implies that in some cases you can convert internal map values from pointers to structs to the structs themselves without any code changes or errors, and in some cases you can't.

(However, with pointer map values the m["nosuchkey"].field case would be a runtime panic. When you deal with explicit pointers, Go makes you accept this possibility.)

This also affects method calls (and method values) in some situations, because of this special case:

[...] If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m(): [...]

If you have a type T and there is a pointer receiver method *T.Mp(), you can normally call .Mp() even on a non-pointer value:

var v T
v.Mp()

However, this requires that the value be addressable. Since map values are not addressable, the following is an error (when the type of map values is T):

m["key"].Mp()

Currently, you get two errors for this (reported on the same location):

cannot call pointer method on m["key"]
cannot take the address of m["key"]

This is the same error message as we saw for function return values in my original entry, just about a different thing. As before, converting the map value type from T to *T will make this not an error and all of the syntax is exactly the same.

As with the field access case, Go not allowing this means that it doesn't have to consider what to do if you write:

m["nosuchkey"].Mp()

While there are various plausible options for what could happen here if Go accepted it, I think the one that most people would expect is that it would work the same as:

t := m["nosuchkey"]
t.Mp()
m["nosuchkey"] = t

Which is to say, Go would have to materialize a value and then add it to the map. As a subtle issue, the working version makes it clear when m["nosuchkey"] actually exists. This also makes it explicit that the method call isn't manipulating the value that is in the map.

(My original entry was sparked by a Dave Cheney pop quiz involving the type of a function return, so I was thinking more about function return values than other sorts of values.)

PS: I think this lack of map value addressability means that there's no way today in Go to directly modify a map value or its fields. Instead you must copy the map value into a temporary, manipulate the temporary, and then put it back in the map. This is probably a feature.

programming/GoAddressableValuesII written at 23:33:18; Add Comment


Page tools: See As Normal.
Search:
Login: Password:
Atom Syndication: Recent Pages, Recent Comments.

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.