Go Maps and the Hidden Cost of Assignment
Working with maps in Go starts off feeling direct. You set one up, assign some values, and pull them out by key. That part makes sense, but when you drop in structs or slices, things start behaving in ways that don’t match what you had in mind. You write a few lines that look fine, and suddenly updates aren’t sticking, or values aren’t what you expect. This happens because of how assignment works and how Go handles values behind the scenes. Variables in Go follow clear rules when they’re passed around, and those rules affect how things behave once they land in a map. Structs, slices, and pointers all follow different patterns. If you’re not keeping track of how those values move and what gets copied, it’s easy to miss the reason a change didn’t carry through.
How Assignment Works for Different Value Types
Go handles values in a way that’s meant to be predictable, but that doesn’t always match what you’re thinking when you write the code. When you assign a value into a map, Go always copies that value. If the value itself is a pointer or a slice header, then the copy still points to the same backing data as the original variable. That’s why some types feel like they behave by reference even though the assignment itself is still a copy.
These types don’t behave the same. A struct looks tied to your variable until it isn’t. A slice looks like it was replaced in the map, but that only holds when the variable itself is reassigned. The differences are easy to miss until the result throws off your logic.
Structs Are Copied by Value
When a struct goes into a map, Go creates a full copy. It doesn’t store a reference. It doesn’t keep a shared connection. It just copies the value. Changing the original struct later doesn’t affect what’s stored in the map, because that copy was already locked in when you did the assignment.
Here, s
gets placed in the map with its original value. After that, the map has no connection to the s
variable. Any edits to s
won’t carry over, and the map stays unchanged. That separation can lead to bugs when you expect the struct in the map to reflect the latest updates. It also comes up when you try to change a field directly on a struct inside the map. Go doesn’t allow it, and the compiler throws an error. To make a change, you have to copy the value out, modify it, then store it back in. That extra step makes sense once you realize you’re no longer working with the original value.
Slices Use Shared Backing Arrays
Slices work differently. When a slice goes into a map, Go copies its header. That header points to the array underneath. So both the original variable and the map entry can still reach the same data through that shared pointer.
Changing a value inside the slice affects both views. Replacing the whole slice with a new one cuts the link.
The updated value in the original slice shows up through the map too. That happens because both are still tied to the same array. But that ends as soon as the original variable is reassigned.
After the reassignment, the variable points to a new slice, and the map keeps pointing to the old one. That disconnect often leads to confusion when you think the map reflects your latest changes but it doesn’t. Also, changing the size of the slice, trimming it, or assigning a new slice won’t update the map unless you store that change back in.
Pointers Keep Shared Access Alive
If you want a map and a variable to stay in sync, store a pointer. That gives you direct access to the same memory from both sides. Any update made from one place will show up in the other.
For this, both the map entry and the variable reach the same value. That means you can update either, and both will reflect the change without extra work.
It also means you can change the fields through the map directly.
This is because nothing was copied. The map holds the pointer, and the pointer gives you access to the real struct. That behavior becomes helpful when your program needs to update values across different layers without juggling new assignments or tracking sync issues.
Unexpected Behavior from Composite Types in Maps
Maps work well when you’re dealing with basic values. When composite types come into play, the behavior starts to shift in ways that don’t always match your mental model. Structs, slices, and nested types behave differently depending on how they’re stored and what you try to change. Some edits seem like they should work but don’t, and others work in some cases but break when the structure changes. What looks like a small detail in assignment rules can lead to code that quietly stops doing what you expect, especially when you’re updating something in place or trying to reach into a value inside a value.
Updating Fields Inside Map-Stored Structs Does Not Work Directly
Go won’t let you change a field on a struct that’s stored directly in a map. The compiler catches it and gives you an error, which can be confusing if you thought the map gave you full access to the stored value. What actually happens is that Go blocks the update because you’re working with a copy. It’s not possible to reach into that copy and change part of it without replacing the whole thing.
The items["pen"]
gives you a copy of the struct, and you can’t change a field on it directly. The compiler shuts it down because the value isn’t addressable inside the map context.
To make that change, you have to do it in steps. First, copy the value out of the map into a variable, then update the field, and assign the updated value back into the map. That works because the map only deals with full values when storing and retrieving.
This applies to more than just updates. If you’re looping over a map of structs and try to update fields during that loop, you’ll end up working with temporary copies. Any change made that way won’t stick unless you take the value out, update it, and store it back.
Nested Structs Don’t Behave Like References Either
Even if your map entry points to a struct that contains other structs, the lower levels don’t behave like references. If the top-level struct is stored by value, then everything inside it follows that same pattern. The copy of the struct inside the map carries its own embedded values, and none of those are shared with your original variable.
The attempt to flip the Enabled
flag directly on the map entry fails, not because of the field itself, but because data["entry"]
returns a full copy of Outer
. The compiler won’t let you update part of that copy. Just like with the previous case, you have to pull out the value, change it, and then push it back in.
This gets more noticeable when the structs get larger or more deeply nested. It’s easy to think you’re changing the data inside the map, but unless you’re working with pointers, that change never reaches the actual value. You’re just editing a temporary copy that disappears after the statement runs.
That behavior can be frustrating if you’re writing code that modifies configuration or accumulates state in a map over time. The pattern of copy-edit-replace adds a bit of friction, but it’s necessary any time you’re dealing with values stored directly inside the map.
Appending To A Slice In A Map Works, But Can Get Subtle
Slices in maps act like they behave naturally, right up until you run into something that depends on the capacity behind them. At first, appending to a slice that lives in a map works fine. You can grow the list, and the new items show up as expected. The problem only appears when the slice grows past its original capacity. That’s when Go creates a new backing array, and the pointer inside the map no longer reflects the new data unless you’ve reassigned it properly.
This works because the original capacity allowed for the slice to grow without reallocating. If the capacity had been reached earlier, Go would’ve created a new array behind the scenes. At that point, any other references to the old slice would still point to the original array, while the new one lives only inside the map.
Here’s a case where that reallocation breaks the connection between two views of the same slice.
Even though you updated original[0]
, that change doesn’t show up in the map anymore. That’s because the append
call caused a reallocation, and now original
points to a different array than the one stored in the map. From that point on, the two values are completely separate.
It’s one of those behaviors that feels quiet when it happens. You don’t get an error, nothing breaks loudly, but the values don’t match anymore. If you’re keeping slices in a map and modifying them elsewhere, that disconnect can build up silently unless you’re always writing the final slice back into the map after each change.
Conclusion
Maps in Go behave in simple ways on the surface, but the mechanics behind assignment can cause trouble if you’re not watching how values are stored. Structs are copied, slices keep a shared pointer to their backing arrays, and pointers carry updates across both sides. Those differences change how updates work, and the way values move through your code depends entirely on how they’re passed in.