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.copyputs 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.
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:
arris created with length3and contains[1 2 3].len(arr)evaluates to3.make([]int, 3)createstmpwith length3, initially[0 0 0].copy(tmp, arr)compares the two lengths:len(tmp)is3andlen(arr)is3.copytransfersmin(3, 3)elements, so it copies3values.nreceives3.tmpis 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)
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
| Operation | Changes destination length? | Creates independent backing storage? | Best use |
|---|---|---|---|
copy(dst, src) | No | Only if dst was separately allocated | Copy into an existing, sized buffer |
append([]T(nil), src...) | Yes, through assignment | Yes for a non-empty source | Concise slice duplication |
dst := make([]T, len(src)); copy(dst, src) | No after allocation | Yes | Clear, explicit duplication |
dst := src | No | No | Another slice view of the same data |
copy and append solve slightly different problems:
- Use
copywhen you already have a destination slice or buffer and want to overwrite its existing elements. - Use
appendwhen you want to grow a slice with new elements. - Use assignment only when sharing the same underlying data is intentional.
Cheat Sheet
// Copy as many elements as fit in dst.
n := copy(dst, src)
copycopiesmin(len(dst), len(src))elements.copydoes not extend or append todst.- A destination with
len(dst) == 0receives zero elements, even if it has spare capacity. copyreturns 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.
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.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.