Go Deep Copy vs Shallow Copy
The Point
A shallow copy copies the “reference to the data” – the original and the copy still share the underlying data. A deep copy copies “the data itself” – the two are completely independent. Go’s assignment is shallow copy by default; whether the original is affected depends on whether the type is a value type or a reference type.
Explanation
Value type vs Reference type
| Value type (assignment = full copy) | Reference type (assignment = copy reference) |
|---|---|
int, float64, bool, string | slice, map, pointer, channel |
array ([N]T) | interface |
struct (but fields may contain references) |
Slice: the most common gotcha
1a := []int{1, 2, 3}
2b := a // shallow copy: b and a share the same underlying array
3
4b[0] = 99
5fmt.Println(a) // [99 2 3] <- a was modified!a → [ header: ptr → [99, 2, 3], len=3, cap=3 ]
b → [ header: ptr ↗ ] ← same arrayTo deep copy a slice, use copy():
1c := make([]int, len(a))
2copy(c, a) // deep copy
3
4c[0] = 0
5fmt.Println(a) // [99 2 3] <- a is not affectedMap: assignment also only copies the reference
1m1 := map[string]int{"a": 1}
2m2 := m1 // shallow copy
3
4m2["a"] = 99
5fmt.Println(m1["a"]) // 99 <- m1 was also modified
6
7// deep copy a map by looping manually
8m3 := make(map[string]int)
9for k, v := range m1 {
10 m3[k] = v
11}Struct: value copy by default, but watch out for pointer/slice fields
1type Person struct {
2 Name string
3 Scores []int // slice is a reference type!
4}
5
6p1 := Person{Name: "Alice", Scores: []int{90, 80}}
7p2 := p1 // struct is value-copied, but Scores only copies the slice header
8
9p2.Name = "Bob"
10fmt.Println(p1.Name) // "Alice" <- string is a value type, not affected
11
12p2.Scores[0] = 100
13fmt.Println(p1.Scores[0]) // 100 <- the underlying array of Scores is shared!To deep copy a struct with slices, explicitly copy each reference field:
1p3 := Person{
2 Name: p1.Name,
3 Scores: make([]int, len(p1.Scores)),
4}
5copy(p3.Scores, p1.Scores)copy() limitation: only copies the outer layer
copy() is a built-in function, but it only copies element values – it does not recursively handle nested reference types.
1// Elements are value type → copy() works as a deep copy
2a := []int{1, 2, 3}
3b := make([]int, len(a))
4copy(b, a) // fully independent, no problem
5
6// Elements are reference type → copy() only copies the outer layer, inner layer is still shared
7a := [][]int{{1, 2}, {3, 4}}
8b := make([][]int, len(a))
9copy(b, a) // b[0] and a[0] still point to the same underlying array!
10
11b[0][0] = 99
12fmt.Println(a[0][0]) // 99 <- still modifiedConclusion: copy() is sufficient when elements are value types; when elements contain reference types, you need to copy recursively by hand.
Knowledge Sugar
Go has no built-in deep copy
For complex nested structures, there is no silver bullet. Common approaches:
| Method | Best for | Downside |
|---|---|---|
| Manual field-by-field | Simple structures, performance-sensitive | Tedious, must update when fields are added |
encoding/json marshal then unmarshal | Quick validation / performance not critical | Slow, only copies exported fields |
proto.Clone() | Protobuf messages | Only works for proto |
| Third-party library | Complex structures | Adds a dependency |
Interview essential: the underlying structure of slice assignment
A Go slice is essentially a struct:
1// Conceptually looks like this (not actual Go code)
2type SliceHeader struct {
3 Data uintptr // pointer to the underlying array
4 Len int
5 Cap int
6}b := a copies this header, not the underlying array – that is the root cause of shallow copy.
One-sentence test
If modifying the copy also changes the original, it is a shallow copy. If not, it is a deep copy.