4.2 Go Slices: Shallow vs. Deep Copy by Example

4.2 Go Slices: Shallow vs. Deep Copy by Example

TL;DR

  • Assigning a slice to another (sliceB = sliceA) does not create a copy. Both variables will point to the exact same underlying data.
  • If you change an element in sliceB, the change will also appear in sliceA, leading to nasty bugs. ๐Ÿ›
  • To create a truly independent duplicate of a slice, you must use the built-in copy() function.
  • Think of it as giving someone a link to your Google Doc versus sending them a separate PDF file. One lets them edit your original; the other doesn't.

Why This Matters

In programming, we copy data all the time. But in Go, copying slices has a hidden "gotcha" that trips up almost every newcomer. When you think you've made a safe copy, you've actually created a "linked twin." Modifying one accidentally damages the other, causing unexpected behavior that can be incredibly hard to track down.

Understanding this one concept will save you hours of future debugging and help you write more predictable, solid code. Let's break down how to do it right.


Concepts in Plain English

Before we dive in, let's clarify a few key ideas with some simple analogies.

  • Slice: Think of a slice as a window frame looking at a portion of a larger mural. You can change the size of the frame or move it around, but it's always looking at the same underlying painting.
  • Underlying Array: This is the mural itselfโ€”the actual, fixed storage where the data lives. Multiple slices (window frames) can look at the same mural.
  • Shallow Copy (Assignment with =): This is like creating a second window frame and placing it right on top of the first one. Both frames look at the exact same part of the mural. If someone paints over the mural through one window, the view from the other window changes too.
  • Deep Copy (Using copy()): This is like hiring an artist to paint a brand-new, identical mural on a different wall and then giving you a window frame for it. Now you can paint over your new mural all you want, and the original remains untouched.

Mini-Glossary

TermSimple DefinitionWhere It Shows Up
SliceA flexible view into a list of data.The main variable type we're working with, e.g., []int.
Underlying ArrayThe hidden, fixed-size data block that a slice uses for storage.The source of truth that a shallow copy points to.
Shallow CopyCreating a new slice header that points to the same underlying array.sliceB := sliceA
Deep CopyCreating a new underlying array and copying the values from the old one.copy(destination, source)

Do It Step by Step

Let's see the trap and the solution in action. You'll just need Go installed on your machine to run these snippets.

Part 1: The Trap โ€” A Shallow Copy with =

Our goal here is to demonstrate how a simple assignment (=) creates a linked copy that leads to unintended changes.

Check both the original and the copy.Let's see what happened to our original recipe.

// ... inside main()
fmt.Println("\n--- After changing the 'copy' ---")
fmt.Println("Original Recipe:", originalRecipe) // The original is now changed!
fmt.Println("Stolen Recipe:  ", stolenRecipe)

Now, modify the "copy."Our friend decides to replace "eggs" with "butter."

// ... inside main()
stolenRecipe[2] = "butter" // Change the third ingredient in the new slice

"Copy" it using a simple assignment.We'll give this "copy" to a friend who wants to tweak it.

// ... inside main()
stolenRecipe := originalRecipe // This doesn't actually copy the data!
fmt.Println("Stolen Recipe:  ", stolenRecipe)

Create an original slice.Let's make a list of numbers representing recipe ingredients.

// main.go
package main

import "fmt"

func main() {
    originalRecipe := []string{"flour", "sugar", "eggs"}
    fmt.Println("Original Recipe:", originalRecipe)
}

What you should see:

Original Recipe: [flour sugar eggs]
Stolen Recipe:   [flour sugar eggs]

--- After changing the 'copy' ---
Original Recipe: [flour sugar butter]  // Oh no! Our original is ruined!
Stolen Recipe:   [flour sugar butter]

This is the trap! Because stolenRecipe just points to the same underlying data as originalRecipe, changing one affects the other.

Part 2: The Solution โ€” A Deep Copy with copy()

Our goal now is to create a truly independent copy so we can modify it without any side effects.

Check both slices again.This time, our original should be safe.

// ... inside main()
fmt.Println("\n--- After changing the safe copy ---")
fmt.Println("Original Recipe:", originalRecipe) // It's safe!
fmt.Println("Safe Copy:      ", safeCopy)

Modify the new, safe copy.Let's make the same change as before.

// ... inside main()
safeCopy[2] = "butter"

Use the copy() function.The syntax is copy(destination, source).

// ... inside main()
copy(safeCopy, originalRecipe) // This copies the values, not the reference.
fmt.Println("Safe Copy:", safeCopy)

Create a new slice to be the destination.The copy() function needs a destination to copy the elements into. We use make() to create a new slice of the same type and length.

Note โ€” make([]string, len(originalRecipe)) creates a new, empty container perfectly sized to hold a copy of our recipe.
// ... inside main()
safeCopy := make([]string, len(originalRecipe))

Create the original slice (same as before).

// main.go
package main

import "fmt"

func main() {
    originalRecipe := []string{"flour", "sugar", "eggs"}
    fmt.Println("Original Recipe:", originalRecipe)
}

What you should see:

Original Recipe: [flour sugar eggs]
Safe Copy:       [flour sugar eggs]

--- After changing the safe copy ---
Original Recipe: [flour sugar eggs]  // Phew! The original is untouched.
Safe Copy:       [flour sugar butter]

This is the correct and safe way to duplicate a slice when you intend to modify it.

Method Comparison

MethodHow It WorksWhen to Use
Assignment (=)Creates a new slice header pointing to the same data.When you intentionally want multiple variables to manage the same list of data for performance.
copy() functionCreates a new, separate slice and copies each element's value over.When you need an independent duplicate of a slice that can be modified without side effects.

Examples

Here are two complete, runnable examples showing the difference.

Example A: The Dangerous Shallow Copy

This function takes a slice of scores, tries to cap them at 100, but accidentally modifies the original scores list because it doesn't use copy().

package main

import "fmt"

// This function has a bug! It modifies the original slice.
func capScores(scores []int) {
	// This creates a shallow copy. It points to the same data as `originalScores`.
	capped := scores
	for i, score := range capped {
		if score > 100 {
			capped[i] = 100 // This changes the original slice!
		}
	}
	fmt.Println("Inside function, scores are:", capped)
}

func main() {
	originalScores := []int{88, 105, 92, 110}
	fmt.Println("Before function call:", originalScores)

	capScores(originalScores)

	fmt.Println("After function call: ", originalScores) // The original data is now corrupted.
}

Example B: The Safe Deep Copy

Here's the corrected version. By creating a new slice and using copy(), we can safely modify the data inside the function without affecting the original caller.

package main

import "fmt"

// This function is safe! It works on a true copy.
func capScoresSafely(scores []int) {
	// 1. Create a new slice to hold the copied data.
	capped := make([]int, len(scores))
	// 2. Copy the values from the original into the new slice.
	copy(capped, scores)

	for i, score := range capped {
		if score > 100 {
			capped[i] = 100 // This only changes the 'capped' slice.
		}
	}
	fmt.Println("Inside function, scores are:", capped)
}

func main() {
	originalScores := []int{88, 105, 92, 110}
	fmt.Println("Before function call:", originalScores)

	capScoresSafely(originalScores)

	fmt.Println("After function call: ", originalScores) // The original data is safe!
}

Visualizing the Difference

This diagram shows how the two methods relate to the underlying data.

flowchart LR
    subgraph "Shallow Copy (b = a)"
        direction LR
        a[Slice 'a'] --> data(Underlying Data)
        b[Slice 'b'] --> data
    end

    subgraph "Deep Copy (copy(dest, src))"
        direction LR
        src[Slice 'src'] --> data1(Original Data)
        dest[Slice 'dest'] --> data2(Copied Data)
        src -- "copies values to" --> dest
    end

In a shallow copy, both variables point to one data source. In a deep copy, a second, independent data source is created.


Common Pitfalls

  • Forgetting copy() exists. The most common mistake is simply using b = a and assuming it creates a new copy. Always pause and ask: "Do I need an independent copy?" If yes, use copy().
  • Slicing a slice still shares data. Creating a sub-slice like sub := original[1:3] also creates a shallow copy. It's a new "window frame," but it's looking at the same original mural. Changes to sub will affect original.

Destination slice is too small. The copy() function will only copy as many elements as the shorter of the two slices can hold. If your destination slice is empty or too small, you won't get a full copy.Go

// Pitfall! `dest` has length 0, so nothing is copied.
src := []int{1, 2, 3}
dest := make([]int, 0)
copy(dest, src) // `dest` is still []

FAQ

1. Is assigning slices (=) always bad?

Not at all! It's very efficient. You just have to know what it does. If you want two variables to refer to and manipulate the same list, it's the perfect tool. The danger only comes from thinking it creates a separate copy when it doesn't.

2. Is there a shorter way to make a full copy?

Yes, advanced Go programmers sometimes use append as a one-liner for copying: safeCopy := append([]string(nil), originalRecipe...). It works well, but using make and copy is often clearer for beginners to read.

3. What does copy() return?

It returns an integer telling you how many elements were copied. This is useful for confirming that your copy was successful and complete.

4. Does this apply to other types like maps or channels?

Yes and no. Maps and channels are also reference types, so assigning them (mapB = mapA) also creates a reference, not an independent copy. However, they don't have a simple copy() function like slices do; copying them requires iterating over them manually.


Recap

Let's wrap up with the key takeaways.

  • Assigning a slice with = creates a reference, not a copy. Both variables will point to the same memory.
  • To create a truly independent slice, you must allocate new memory (with make()) and copy the elements over (with copy()).
  • Always be mindful when passing slices to functions. If the function needs to modify the data without affecting the original, it should work on a copy.
  • Next time you write newSlice := oldSlice, ask yourself: "Do I want a mirror or a clone?" If you want a clone, use copy().