Understanding Addressability in Go: Pitfalls with Maps and Structs
Introduction
Let's begin by examining the following code snippet and predicting the output.
type human struct {
age int
name string
}
func main() {
m := make(map[string]human)
h := human{
age: 20,
name: "John",
}
m["foo"] = h
m["foo"].name = "Paul"
fmt.Println(m["foo"].name)
}
If you anticipate the output to be "Paul"
, you're mistaken.
The code won't compile and will throw the following error:
./prog.go:17:2: cannot assign to struct field m["foo"].name in map
This error indicates that it's impossible to assign a new value to the field name
of the struct stored in the map (specifically, m["foo"].name = "Paul"
).
If you've ever programmed in Go, you're likely aware that the expression m["foo"]
returns a value stored at key foo
within map m
. But why does this error occur?
In this post, we'll delve into the cause of the error and explore how to address it.
What causes "cannot assign to struct field in map"?
In essence, this error arises because the struct returned by m["foo"]
is not addressable.
In other words, for a value to be assigned, it must be addressable.
Let's break down what "addressable" means in Go.
what is "addressable" in Go?
The concept of "addressable" is quite straightforward. It implies that an addressable value possesses a memory address and can be accessed via a pointer.
Consider the following code snippet, which clarifies the concept of "addressable" in Go.
var s string
s = "hello"
fmt.Println(s)
fmt.Println(&s)
// output:
// hello
// 0xc000104020
In the first line, we declare a variable s
of type string
. At this point, Go allocates memory for the variable s
.
Subsequently, in the second line, we assign the string value "hello"
to the variable s
. This assignment succeeds because the variable s
is addressable, as evidenced by the fact that fmt.Println(&s)
prints the memory address of the variable s
.
what is NOT addressable in Go?
Conversely, an unaddressable value lacks a memory address and cannot be accessed via a pointer.
Consider the following code snippet:
func main() {
h := newHuman("George")
fmt.Println(h) // {0 George}
h.name = "Ringo" // can modify the field of the struct because h is addressable
fmt.Println(h) // {0 Ringo}
newHuman("Harrison").name = "Starr" // panics!
}
type human struct {
age int
name string
}
func newHuman(name string) human {
return human{
age: 0,
name: name,
}
}
As commented in the code, the code panics at newHuman("Harrison").name = "Starr"
with the following message:
./prog.go:14:2: cannot assign to newHuman("Harrison").name (neither addressable nor a map index expression)
Note: The term "map index expression" mentioned in the error message refers to reading a value from a map like m["foo"], or writing a value to a map like m["foo"] = "bar". If you're wondering why a map index expression is not addressable, I'll explain later in this post.
The reason for the panic is that the value returned by newHuman()
or any function lacks a memory address right after the function call.
The returned value of functions becomes addressable only when assigned to a variable.
Therefore, the code below functions correctly.
func main() {
h := newHuman("George")
fmt.Println(h) // {0 George}
h.name = "Ringo"
fmt.Println(h) // {0 Ringo}
x := newHuman("Harrison") // returned value of newHuman() is assigned to x, so x is addressable
x.name = "Starr" // can modify the field of the struct because x is addressable
}
How is "map index expression" different from "addressable" variable?
Now that we've understood what "addressable" signifies in Go, let's return to the original code snippet that yields the error cannot assign to struct field in map
and ascertain why this error occurs.
type human struct {
age int
name string
}
func main() {
m := make(map[string]human)
h := human{
age: 20,
name: "John",
}
m["foo"] = h
m["foo"].name = "Paul"
fmt.Println(m["foo"].name)
}
As the error message implies, we can infer that the struct returned by m["foo"]
lacks a memory address and is not addressable, despite having assigned a value to the variable h
before storing it in the map.
This is because the value returned by the expression m["foo"]
is merely a copy of the struct stored in the map, not the actual struct you had stored in the map.
Therefore, when attempting to assign a new value to the field of the struct returned by m["foo"]
, it panics because the returned value is not yet addressable.
So, how can we fix this issue?
I'll illustrate two workarounds in the following section.
Workarounds
1. Assign the struct to a variable
The first workaround involves assigning the struct to a variable bar
, then modifying the name
field of the struct. Subsequently, we assign the modified struct back to the map.
func main() {
m := make(map[string]human)
h := human{
age: 20,
name: "John",
}
m["foo"] = h
// m["foo"].name = "Paul"
bar, ok := m["foo"]
if ok {
bar.name = "Paul"
m["foo"] = bar
}
fmt.Println(m["foo"].name)
}
// output:
// Paul
Instead of attempting to modify the field of the struct returned by m["foo"]
directly, we assign the struct to a variable bar
. This makes it possible to modify the field of the struct since bar
is addressable.
2. Use a pointer to struct
This method requires less code than the previous one, and I personally prefer this approach.
func main() {
m := make(map[string]*human)
h := human{
age: 20,
name: "John",
}
m["foo"] = &h
m["foo"].name = "Paul"
fmt.Println(m["foo"].name)
}
// output:
// Paul
The distinction from the initial implementation lies in the fact that the map m
stores a pointer to the struct human
rather than the struct itself.
Thus, m["foo"]
now returns the pointer to the actual struct, unlike the initial implementation where it returned a copy of the struct, which was not addressable.
Additionally, using a pointer is more efficient, especially when the size of the struct is large, as it prevents copying the struct each time it's accessed.
Wrap up
In this post, we've elucidated why the error cannot assign to struct field in map
occurs and how to resolve it.
The error arises because the struct returned by m["foo"]
is not addressable. This can be rectified by assigning the struct to a variable or using a pointer to the struct.
I hope this post aids in comprehending the concept of "addressable" in Go and how to tackle the error cannot assign to struct field in map
.
Happy coding!