Kinoko's TIL Log

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, stringslice, 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 array

To 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 affected

Map: 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 modified

Conclusion: 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:

MethodBest forDownside
Manual field-by-fieldSimple structures, performance-sensitiveTedious, must update when fields are added
encoding/json marshal then unmarshalQuick validation / performance not criticalSlow, only copies exported fields
proto.Clone()Protobuf messagesOnly works for proto
Third-party libraryComplex structuresAdds 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.

#go #til

← Back to Main Page