I got burned by this several days ago that forced me to push to write this blogpost to prevent other people from having the same experience. Before we start with example, let’s have a quick detour to what is shallow and deep copy in go.
Shallow copy in go is an assignment. Everything in go is passed by value so assigning variable with a struct to another variable makes a copy of that struct. The same thing goes if you pass a variable to a function. Method always gets a copy of your variable (if you pass in a pointer, it gets copy of the pointer! — but not copy of the value underneath the pointer).
Deep copy in go is something that you use when shallow copy is not enough (considering the headline of the article but not giving any spoilers — this is what you should use pretty much all the time, when you want to make sure you have a copy). There is nothing built into the language, you need to use libraries, for example copier.
So let’s go back to the original headline of the article and continue with an example:
Just by looking at that struct, if you want to keep it immutable and be sure you have a new copy in hand, would you use shallow or deep copy? What about when you embed it with another struct?
And what about if you suddenly need to store multiple addresses next to a person?
If you make the same mistake I did, you start with a shallow copy because why not, it’s enough. That is actually perfectly valid solution for the first example, as well as the example with embedded structs. Where it all breaks is the third one (and that is just one of the many examples how this actually can break).
The reason for that is that even though that Go is explicitly saying that it has no reference types there’s multiple types, that are actually fully or partially backed by pointers. Those types are map, pointer, slice, channel, function and interface. So if we take a slice as an example, it actually consists of two properties — len and cap and then a pointer to the actual array (read about slice internals). So when you shallow copy a slice, you actually correctly copy len and cap properties, but your new copy is still pointing to the same array (you copied just the pointer, but it’s still pointing to the same address in memory).
One of the key takeways here is that structs evolve over time. What might have started as something you can safely shallow copy might change few weeks later when someone comes to extend that struct. If you don’t have a very detailed tests, there is a high chance that your shallow copy won’t give you the full copy you expect and then things mysteriously break in production. That’s why I made a pact with myself to never rely on shallow copy for structs.