The copy Built-in
How Go's built-in copy function transfers elements between slices, handles overlapping memory, and avoids common pitfalls.
The copy function is Go's safe, built-in way to move slice elements from one backing array to another without sharing storage. A plain assignment between two slice variables copies only the slice header — the pointer, length, and capacity — so both variables end up pointing to the same underlying data. copy writes the actual element values into a destination slice, giving you two slices that can evolve independently.
Signature and Return Value
func copy(dst, src []Type) int
copy takes a destination slice and a source slice with the same element type. It returns the number of elements actually copied, which is always the smaller of len(dst) and len(src). The function never changes the length of either slice — it only overwrites elements that already exist in the destination.
src := []int{10, 20, 30, 40, 50}
dst := make([]int, 3)
n := copy(dst, src)
fmt.Println(dst) // [10 20 30]
fmt.Println(n) // 3
dst was created with length 3, so only the first three elements of src land there. The return value 3 confirms that three elements were copied. The source slice is unchanged, and the destination's backing array now holds the copied values. Nothing about this operation links the two slices — they have separate backing arrays because make allocated a fresh one for dst.
Length matters, not capacity:
copy respects the length of both slices, not their capacity. Even if dst has spare capacity, only elements within len(dst) are overwritten. Extra slots beyond the length are untouched.
Why the Destination Must Be Large Enough
The most frequent mistake with copy is handing it an uninitialized or zero-length destination. A nil slice has length 0, so nothing gets copied.
src := []int{1, 2, 3}
var dst []int // nil, len 0
n := copy(dst, src)
fmt.Println(dst) // []
fmt.Println(n) // 0
The return value 0 is your signal that no elements moved. The source remains intact, but dst stays empty because there was no room to place anything.
copy never grows the destination:
copy does not allocate memory or increase the length of the destination. If you need a slice that holds a full duplicate of the source, you must create a destination with sufficient length beforehand — typically with make([]T, len(src)).
The fix is to give the destination the same length as the source:
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println(dst) // [1 2 3]
Now the destination's backing array is large enough to hold every element. This pattern — make followed by copy — is the canonical way to create an independent duplicate of a slice.
Mental model for beginners: Think of copy as pouring water from one glass into another. If the destination glass is too small, water spills and is lost. If it's exactly the right size, the transfer is perfect. The function never magically enlarges the glass.
Overlapping Slices and Self-Copying
copy is explicitly designed to work correctly even when the source and destination slices overlap — that is, when they point into the same underlying array. The language specification guarantees this, and the implementation handles the memory copying order so that data isn't corrupted during the transfer.
A common practical use is shifting elements left within a slice, for example to remove the first element:
s := []int{10, 20, 30, 40}
n := copy(s, s[1:]) // shift everything left by one position
fmt.Println(s) // [20 30 40 40]
fmt.Println(n) // 3
The source s[1:] covers indices 1 through 3, and the destination is the full slice s starting at index 0. After the copy, indices 0, 1, and 2 receive the values 20, 30, and 40. The element at index 3 remains unchanged because it was outside the copy range — only three elements were written. If you then re-slice to drop the trailing duplicate, you'd get the shortened result:
s = s[:len(s)-1] // [20 30 40]
The overlapping region was handled safely because copy internally copies in the correct direction — in this case, it copies forward from the lower indices to the higher ones after the overlap is accounted for.
Overlap doesn't mean the slices stay independent:
Even though copy safely transfers overlapping data, the destination and source still share the same backing array after the operation. If you later append to either slice, you may overwrite shared storage unless a new array gets allocated. This is by design — use copy to rearrange elements in place, but create a fresh slice if you need true isolation.
A self-copy that shifts elements right needs care with the direction. If you try copy(s[1:], s), the overlap could cause the source to overwrite data that hasn't been read yet. Go still handles this correctly — the implementation automatically chooses the right copy direction based on the positions of source and destination.
s := []int{1, 2, 3, 4}
copy(s[1:], s) // shift right by one? Actually, this may duplicate.
fmt.Println(s) // [1 1 2 3]
Here the destination starts one element to the right of the source, so the first copy writes s[0] (which is 1) into s[1]. Then s[1] (now 1) gets read as the second element of the source and written into s[2], and so on. The result is a cascading duplication. This isn't a bug in copy — it's the logical outcome of overlapping memory with forward shift. The function did exactly what was asked. If you want to insert a gap and shift elements right, you typically need to ensure the source and destination don't overlap or use a different technique.
Special Case: Copying from a String to a Byte Slice
copy accepts one special argument combination: the destination can be a []byte slice and the source can be a string. This copies the bytes of the string into the byte slice.
var buf [8]byte
n := copy(buf[:], "Hello")
fmt.Println(string(buf[:n])) // "Hello"
fmt.Println(n) // 5
The string is treated as a source of bytes. Only as many bytes as fit into the destination length are copied, just like with any other source. This is a convenient, zero-allocation way to extract a prefix of a string into a fixed-size buffer.
String to byte slice only:
The special case works in only one direction: copy(dst []byte, src string). You cannot copy a []byte into a string using copy — strings are immutable in Go, so there is no way to write bytes into one.
Copying vs append for Duplicating Slices
The make-and-copy pattern is not the only way to create an independent duplicate. You can also use append with an empty slice:
src := []int{1, 2, 3}
// Option 1: make + copy
dst1 := make([]int, len(src))
copy(dst1, src)
// Option 2: append to nil
dst2 := append([]int(nil), src...)
Both produce a new slice with the same elements and a separate backing array. The difference lies in capacity.
make + copy creates a slice whose length and capacity equal len(src). No extra room is left at the end, so appending to dst1 immediately allocates a new array.
append on a nil slice allocates a new backing array with enough capacity to hold all the elements — but the capacity is often slightly larger than the length because append overallocates to amortize future growth. If you plan to append more elements later, this can save allocations. If you need an exact-size duplicate and memory predictability matters, prefer make + copy.
src := make([]int, 1000)
dstCopy := make([]int, len(src))
copy(dstCopy, src)
fmt.Println(cap(dstCopy)) // 1000
dstAppend := append([]int(nil), src...)
fmt.Println(cap(dstAppend)) // likely 1024 or similar, depending on Go version
A clear winner for independent duplicates:
Starting with Go 1.21, the standard library offers slices.Clone(src), which returns an independent copy with exactly the same length and capacity as the source. It's the cleanest option when you have no reason to control the capacity manually.
Common Pitfalls
Several mistakes show up regularly when developers first use copy. Knowing them ahead of time prevents hard-to-spot bugs.
Assuming copy allocates a destination. It never does. If the destination is nil or has length zero, nothing gets copied. Always pre-allocate with make or ensure the destination already has enough length.
Expecting copy to truncate the destination. If len(dst) is larger than len(src), the trailing elements of dst retain their original values. copy overwrites only the first len(src) positions.
dst := []int{9, 9, 9, 9, 9}
src := []int{1, 2}
copy(dst, src)
fmt.Println(dst) // [1 2 9 9 9]
Using copy when you meant to share. If you actually want two variables to reference the same underlying data (for example, to pass a window into a large buffer), a simple slice expression or assignment is appropriate. copy physically moves data and severs that connection.
Forgetting the return value. The number of elements copied is often a useful signal — especially when you aren't sure of relative lengths at compile time. Ignoring it means you won't notice when a partial copy occurred.
Overlapping confusion and stale elements:
When working with overlapping copies, leftover elements beyond the copy count stay in the slice. For example, after shifting left with copy(s, s[1:]), the last element is a duplicate of the previous one until you explicitly shrink the slice with s = s[:len(s)-1]. Failing to re-slice leaves stale data that can cause subtle logic errors.
Practical Use Cases
copy appears in several real-world patterns that every Go developer encounters.
Returning a defensive copy from a function. When a function returns an internal slice to outside callers, you often want to prevent the caller from mutating the original data. A make + copy at the return boundary gives the caller a safe, independent copy.
func AllUsers() []string {
users := []string{"alice", "bob", "charlie"}
c := make([]string, len(users))
copy(c, users)
return c
}
Implementing Cut and Delete without memory leaks. The Go wiki's slice tricks page shows how copy can remove a range of elements without leaving the old values referenced by the backing array. By copying the tail of the slice over the removed segment and clearing the now-unused slots, you avoid holding onto memory longer than necessary.
// Cut elements from i to j
copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k < n; k++ {
a[k] = 0 // zero value for element type
}
a = a[:len(a)-j+i]
Working with fixed-size buffers for I/O or parsing. A []byte buffer is often reused across read operations. copy lets you preserve a prefix or shift remaining data to the front of the buffer before the next read, avoiding extra allocations.
buf := make([]byte, 1024)
// After reading n bytes and processing p bytes...
copy(buf, buf[p:n]) // shift unprocessed data to the front
// buf[0:n-p] now holds the remaining data, ready for the next read.
Each of these relies on copy's guarantee of safe overlapping behavior and its property of writing exactly the number of elements that fit — no more, no less.
Summary
copy is a small function with a precise job: transfer element values from a source slice to a destination slice without altering either slice's length or ever allocating memory. That precision makes it dangerous when the destination is too short — no elements move, and no error is raised — but it's also what makes copy predictable and zero-overhead in tight loops, parsers, and in-place transformations.
The single most important habit to build is always pairing copy with a destination that has the right length. Whether that comes from make, an array slice with a known size, or an existing buffer, the destination's length is the contract. After that, overlap, self-copying, and the string-to-bytes special case all behave exactly as documented.
When you need an independent duplicate of a slice and don't care about fine-tuning the capacity, make + copy is the idiom. For a concise alternative in Go 1.21 and later, slices.Clone wraps this pattern. And when you need to rearrange elements within the same backing array — removing elements, shifting, filtering — copy is the building block you'll keep coming back to.
Next, the chapter moves to two-dimensional and nested slices, where the copy semantics get more interesting because each inner slice is itself a reference type.