Question

Copying Slices in Go: Why copy() Needs a Destination Length

goslice

Question

I need to duplicate a slice in Go. The documentation says that the copy built-in copies elements from a source slice into a destination slice and returns the number of copied elements.

Why does this code leave tmp empty?

arr := []int{1, 2, 3}
tmp := []int{}

copy(tmp, arr)

fmt.Println(tmp)
fmt.Println(arr)

The output is:

[]
[1 2 3]

Why can I not copy the elements from arr into tmp?

Short Answer

You will learn that copy writes only into elements that already exist in the destination slice. An empty slice has length 0, so copy has no destination elements to overwrite. You will also see reliable ways to create an independent slice copy.

Concept

A Go slice is a small descriptor that refers to an underlying array. It has three important properties:

  • Pointer: where its underlying data begins
  • Length (len): how many elements are currently part of the slice
  • Capacity (cap): how much room exists before a new array may be needed

The built-in copy(dst, src) copies into the existing elements of dst. It does not append elements and does not change dst's length.

The number of copied elements is:

min(len(dst), len(src))

In the question, tmp := []int{} creates a slice with length 0. Therefore:

copy(tmp, arr) // copies min(0, 3), which is 0

To duplicate a slice, create a destination whose length is large enough first:

tmp := make([]int, len(arr))
copy(tmp, arr)

This matters because Go makes the amount of data written explicit. It prevents a function such as copy from silently changing a slice's size.

Mental Model

Think of a slice as a row of numbered boxes.

  • len(slice) is the number of boxes currently available to use.
  • cap(slice) is the total space that may be available behind the scenes.
  • copy puts values into existing boxes; it does not create new boxes.

An empty slice has zero usable boxes:

tmp: []

So there is nowhere for copy to place the values. Creating make([]int, 3) gives tmp three boxes, allowing copy to fill them.

Take Quiz

Syntax and Examples

The general syntax is:

count := copy(destination, source)

count is the number of elements copied.

Create a destination with the required length:

package main

import "fmt"

func main() {
    arr := []int{1, 2, 3}

    tmp := make([]int, len(arr))
    copied := copy(tmp, arr)

    fmt.Println(tmp)    // [1 2 3]
    fmt.Println(copied) // 3
}

make([]int, len(arr)) creates a slice containing three zero-value integers first:

[0 0 0]

Then copy overwrites those positions with values from arr.

A concise alternative uses append:

tmp := append([]int(nil), arr...)

Appending arr... to a nil slice creates a new slice containing the same elements.

Step by Step Execution

Consider this code:

arr := []int{1, 2, 3}
tmp := make([]int, len(arr))
n := copy(tmp, arr)

Step by step:

  1. arr is created with length 3 and contains [1 2 3].
  2. len(arr) evaluates to 3.
  3. make([]int, 3) creates tmp with length 3, initially [0 0 0].
  4. copy(tmp, arr) compares the two lengths: len(tmp) is 3 and len(arr) is 3.
  5. copy transfers min(3, 3) elements, so it copies 3 values.
  6. n receives 3.
  7. tmp is now [1 2 3].

If the destination is shorter, copying stops at its length:

src := []int{10, 20, 30}
dst := make([]int, 2)

n := copy(dst, src)
fmt.Println(dst) // [10 20]
fmt.Println(n)   // 2

Real World Use Cases

Slice duplication is useful whenever code needs an independent collection of values.

  • Protecting caller data: Copy an input slice before sorting or changing it.
  • Creating snapshots: Preserve a list of IDs, scores, or configuration values at a point in time.
  • Building API responses: Return copied data so callers cannot modify internal slice storage.
  • Buffer processing: Copy bytes from a received network buffer before the buffer is reused.
  • Concurrent workflows: Give a goroutine its own data when shared mutation would be unsafe.

For example, sorting a copied slice avoids changing the original:

sortedScores := make([]int, len(scores))
copy(sortedScores, scores)
slices.Sort(sortedScores)
Take Quiz

Real Codebase Usage

In production Go code, developers commonly copy slices at ownership boundaries: when accepting mutable input, returning internal state, or retaining data from a reusable buffer.

Defensive copy in a constructor

type Team struct {
    members []string
}

func NewTeam(members []string) Team {
    copiedMembers := append([]string(nil), members...)
    return Team{members: copiedMembers}
}

This prevents later changes to the caller's members slice from changing the team's internal data.

Return a copy of internal state

func (t Team) Members() []string {
    return append([]string(nil), t.members...)
}

Copy into a preallocated buffer

When a destination buffer already exists, copy is especially useful:

header := make([]byte, 4)
n := copy(header, packet)
if n < len(header) {
    return errors.New("packet is too short")
}

This pattern combines allocation control with validation based on the return value of copy.

Common Mistakes

Using an empty destination slice

This copies zero elements because tmp has length zero:

arr := []int{1, 2, 3}
tmp := []int{}
copy(tmp, arr) // copies 0 elements

Fix it by allocating the needed length:

tmp := make([]int, len(arr))
copy(tmp, arr)

Confusing capacity with length

This slice has capacity for three values but still has length zero:

tmp := make([]int, 0, 3)
copy(tmp, arr) // still copies 0 elements

Use append with this form:

tmp := make([]int, 0, len(arr))
tmp = append(tmp, arr...)

Or create it with a length:

tmp := make([]int, len(arr))
copy(tmp, arr)

Assigning a slice instead of copying it

This does not duplicate the elements:

tmp := arr

Both slices refer to the same underlying array. Changing an element through one may affect the other:

tmp[0] = 99
fmt.Println(arr) // [99 2 3]

Expecting a deep copy

Copying a slice copies its elements. If its elements are pointers, maps, slices, or other reference-like values, the referenced data is still shared.

source := [][]int{{1, 2}}
clone := append([][]int(nil), source...)
clone[0][0] = 99

fmt.Println(source[0]) // [99 2]

A deep copy requires copying each nested value as well.

Comparisons

OperationChanges destination length?Creates independent backing storage?Best use
copy(dst, src)NoOnly if dst was separately allocatedCopy into an existing, sized buffer
append([]T(nil), src...)Yes, through assignmentYes for a non-empty sourceConcise slice duplication
dst := make([]T, len(src)); copy(dst, src)No after allocationYesClear, explicit duplication
dst := srcNoNoAnother slice view of the same data

copy and append solve slightly different problems:

  • Use copy when you already have a destination slice or buffer and want to overwrite its existing elements.
  • Use append when you want to grow a slice with new elements.
  • Use assignment only when sharing the same underlying data is intentional.
Take Quiz

Cheat Sheet

// Copy as many elements as fit in dst.
n := copy(dst, src)
  • copy copies min(len(dst), len(src)) elements.
  • copy does not extend or append to dst.
  • A destination with len(dst) == 0 receives zero elements, even if it has spare capacity.
  • copy returns the number of copied elements.
  • Source and destination may overlap.

Duplicate a slice explicitly:

dst := make([]T, len(src))
copy(dst, src)

Or use append:

dst := append([]T(nil), src...)

Preallocate for append:

dst := make([]T, 0, len(src))
dst = append(dst, src...)

Remember: copying a slice of pointers, maps, or nested slices is not automatically a deep copy.

FAQ

Does copy allocate a new slice in Go?

No. copy only writes into the destination slice you provide. Allocate the destination first if needed.

Why does copy return an integer?

It returns the number of elements actually copied. This is useful when either slice may be shorter than expected.

Can I copy into a nil slice?

Yes, but zero elements are copied because a nil slice has length zero.

var dst []int
n := copy(dst, []int{1, 2}) // n is 0

Does make([]int, 0, 10) allow copy to copy ten values?

No. Its capacity is 10, but its length is 0. copy uses length, not capacity. Use append or create it with length 10.

Is append([]int(nil), src...) a valid way to clone a slice?

Yes. It is a common concise way to create a new slice with copied elements.

Does copying a slice make a deep copy?

Usually no. It copies the slice elements. If an element itself refers to mutable data, that underlying data remains shared.

Can source and destination overlap in copy?

Yes. Go explicitly permits overlapping source and destination slices.

Take Quiz

Mini Project

Description

Build a helper that takes a list of product tags and returns a cleaned copy. The caller's original slice must remain unchanged, which is a common requirement when functions normalize, sort, or otherwise modify input data.

Goal

Create a function that duplicates a string slice, trims each tag, and proves that the input slice was not modified.

Requirements

Create a cleanTags function that accepts []string and returns []string. Make an independent copy before changing any tag. Trim leading and trailing whitespace from every copied tag. Print both the original and cleaned slices. Demonstrate that changing the cleaned slice does not change the original slice.

Take Quiz

Keep learning

Related questions

View more