The map value is a copy, not a slot
You're building a cache of user profiles. You store them in a map keyed by user ID. You fetch a user, want to update their email, and write users["alice"].Email = "new@example.com". The compiler stops you cold with cannot assign to struct field in map. You stare at the screen. The syntax looks perfect. Go is refusing to let you touch a field that is right there.
Go maps don't store values in a fixed memory slot you can point to. When you ask for m[key], Go looks up the value, makes a copy, and hands that copy to you. You are holding a photocopy of the struct. Changing the photocopy doesn't change the original document in the filing cabinet. The compiler blocks the assignment because m["key"].Field isn't a variable you can write to. It's an expression that produces a temporary value. You can't assign to a temporary.
Maps hand you copies. Treat them like photocopies, not the original document.
Minimal example
Here's the simplest reproduction: a map of structs, a failed field assignment, and the load-modify-store fix.
package main
type User struct {
ID string
Score int
}
// main demonstrates the error and the fix.
func main() {
// Map stores copies of the struct value.
scores := map[string]User{
"alice": {ID: "alice", Score: 10},
}
// scores["alice"] returns a copy of the User struct.
// This line fails to compile.
// scores["alice"].Score = 20 // Error: cannot assign to struct field in map
// u is a local variable holding that copy.
u := scores["alice"]
// Modify the local copy.
u.Score = 20
// Write the updated copy back to the map.
scores["alice"] = u
}
Why the compiler blocks you
At compile time, Go checks addressability. An index expression like m[key] is not addressable when the map value is a struct. The compiler knows the result is a temporary copy. It rejects any assignment to a field of that temporary. You also cannot take the address with &m["key"]. The compiler rejects that with cannot take the address of m["key"]. Addressability is the root cause. You can only assign to a field if the struct is addressable. Map index expressions return values, not addresses.
Go maps are hash tables that grow dynamically. When a map grows, the runtime allocates a larger table and moves entries to new slots. If Go allowed you to hold a pointer to a map value, that pointer could become invalid the moment the map resizes. The language avoids this class of bugs by making map values non-addressable. You can't accidentally hold a dangling reference to a map entry. The load-modify-store pattern forces you to interact with the map through the map operation, which handles resizing safely.
The compiler protects you from undefined behavior. Respect the error and write back the value.
Realistic update pattern
Here's a helper function that updates a config entry using the safe pattern.
package main
type Config struct {
Timeout int
Retries int
}
// updateConfig updates the Timeout field for a specific key.
func updateConfig(m map[string]Config, key string, newTimeout int) {
// cfg is a copy of the struct stored in the map.
cfg := m[key]
// Modify the local copy, not the map entry.
cfg.Timeout = newTimeout
// Assign the whole struct back to update the map.
m[key] = cfg
}
Load, modify, store. Never skip the store step.
Pitfalls and traps
The compiler error is explicit. You get cannot assign to struct field in map. There is no workaround that lets you assign directly to the field. You must use the load-modify-store pattern or switch to pointers.
A common mistake is loading the value, modifying it, and forgetting to write it back. The change vanishes because you only updated the local copy. Another trap appears with missing keys. If you load a key that doesn't exist, Go returns the zero value of the struct. If you modify that zero value and write it back, you accidentally insert a new entry with partial data. Always check if the key exists before updating, or use the two-value map lookup. When checking for existence, use the two-value form. val, ok := m[key]. If you don't care about the value, use _ to discard it. _, ok := m[key]. This signals to readers that you are only checking for presence.
Here's the zero-value trap: updating a missing key creates a partial entry.
package main
type User struct {
Name string
Age int
}
// main demonstrates the zero-value trap.
func main() {
users := map[string]User{}
// users["ghost"] returns the zero value {Name: "", Age: 0}.
u := users["ghost"]
// u holds that zero value.
// Modifying Age changes the local copy.
u.Age = 30
// Writing back inserts a new entry with the modified zero value.
// The Name field is empty because it came from the zero value.
users["ghost"] = u
}
Always check for existence before updating a value map.
When pointers help
If you find yourself doing load-modify-store constantly, or if you need to share the struct between the map and other parts of the program, switch to a map of pointers. A map of pointers stores addresses. The address stays valid even if the map resizes, because the pointer lives in the map, but the struct lives on the heap. You can mutate fields directly.
Here's how a map of pointers enables direct field mutation.
package main
type Item struct {
Name string
Count int
}
// main demonstrates direct mutation with a map of pointers.
func main() {
// Map values are pointers to structs on the heap.
inventory := map[string]*Item{
"widget": {Name: "widget", Count: 5},
}
// inventory["widget"] returns a copy of the pointer.
// The pointer still references the original struct.
// Modifying the field updates the shared heap object.
inventory["widget"].Count = 10
}
Pointers allow mutation but introduce nil risks. Choose based on your needs.
Decision matrix
Use a map of struct values when the structs are small, immutable, or you only need to replace the entire entry at once. Use a map of pointers when you need to mutate fields in place or share the same struct instance across multiple map entries. Use the load-modify-store pattern when you have a map of values and need to update a single field. Use a mutex alongside the map when multiple goroutines read and write the map concurrently.
Values for safety and simplicity. Pointers for mutation and sharing. Pick the shape that matches your access pattern.