skip to content

What does Go's copy builtin return, and why does copying into make([]int, 0, 5) copy nothing?

level: middleimportance: should knowfreq 58%

answer

  1. it returns a count, not a slice
  2. the shorter of the two decides
  3. length, never capacity
  4. make with a length, not just a capacity
  5. overlap is explicitly allowed

basics

~20 s

copy returns how many elements it moved: the smaller of the two slices' lengths. A destination made with make([]int, 0, 5) has length zero, so there is nowhere to write and copy returns 0. Capacity is ignored.

solid answer

~40 s

`copy(dst, src)` moves `min(len(dst), len(src))` elements and returns that count. It never allocates and never grows `dst`; capacity plays no part. So `make([]int, 0, 5)` is a destination of length zero and `copy` writes nothing and returns 0 - the classic bug, because the slice *looks* big enough. The fix is to size the destination by length: `dst := make([]int, len(src))`. `copy` is safe when the two slices overlap in the same backing array, which is what makes it the tool for shifting elements within a buffer. There is one special form, `copy(byteSlice, someString)`, which copies a string's bytes into a `[]byte`. If you want a fresh slice rather than a fill, `append([]T(nil), src...)` or `slices.Clone(src)` is the idiom.

code

go · 7 lines
go
src := []int{1, 2, 3}

dst := make([]int, 0, 5) // len 0, cap 5
n := copy(dst, src)      // n == 0, nothing written

dst = make([]int, len(src)) // len 3
n = copy(dst, src)          // n == 3, dst is [1 2 3]

go deeper

for a junior

Remember the two facts that matter daily: copy returns how many elements moved, and the destination must already have a length. Making it with make([]T, len(src)) is the habit to form.

for a middle

Explain min(len(dst), len(src)) and why capacity is irrelevant, contrast copy with append and with plain assignment of a slice header, and mention that overlap is well defined.

for a senior

Show where the guarantee earns its keep: sliding the unconsumed remainder of a read buffer to the front with no allocation, and knowing that a copy of a slice of pointers is still shared underneath.

for a principal

Make the shared rule explicit for the codebase: any data crossing a package boundary is cloned rather than aliased, or the ownership is documented on the API. That decision is cheaper to make once than to litigate per pull request.

## The contract `copy(dst, src)` is a built-in with a deliberately blunt contract: - it copies `min(len(dst), len(src))` elements from `src` into `dst`, starting at index 0 of each; - it returns that number of elements; - it never allocates, never resizes `dst`, and never looks at either slice's **capacity**; - it is defined even when `src` and `dst` share a backing array and overlap. Everything surprising about `copy` follows from the second and third bullets. ## The length-versus-capacity trap ```go dst := make([]int, 0, 5) // len 0, cap 5 n := copy(dst, []int{1, 2, 3}) // n == 0, dst is still empty ``` The destination has room for five ints, but its *length* is zero, so there are no valid indices to write to. Writing into spare capacity is exactly the thing `copy` will not do for you - only `append` does that, because only `append` can hand back the longer header that makes the extra elements addressable. A `copy` that silently returns 0 is one of the quieter Go bugs: nothing panics, nothing logs, and the destination is simply empty downstream. The fixes, in order of how often they are right: ```go dst := make([]int, len(src)) // size by length; copy then fills it copy(dst, src) dst := append([]int(nil), src...) // allocate and fill in one step dst := slices.Clone(src) // the same thing, named (Go 1.21+) ``` Using the return value defensively is also good practice when the destination is a fixed window: `if n := copy(window, src); n < len(src) { /* src was truncated */ }`. ## Why overlap is safe The specification guarantees `copy` behaves correctly for overlapping source and destination, so you can shift elements inside one backing array without a scratch buffer. That is the basis of the standard delete idiom: ```go // remove element i, preserving order s = append(s[:i], s[i+1:]...) ``` which is a `copy` of the tail over the hole, plus a shorter header. (`slices.Delete` does the same and is clearer.) It also matters to a pipeline stage that keeps a read buffer between reads: after processing a prefix, `n := copy(buf, buf[consumed:])` slides the unconsumed remainder to the front and `buf = buf[:n]` shortens the window, all without a new allocation. ## The string special case There is one asymmetry in the builtin. Normally both arguments must be slices of the same element type, but `copy(dst []byte, src string)` is also legal - a string's bytes can be copied into a byte slice directly, with no conversion and no intermediate allocation. That is a small but real optimisation when filling a reusable buffer from string data. ## copy versus append versus assignment Three things in Go are all called copying and they are not the same: - **assignment** (`b := a`) copies the three-word slice header. Both variables now describe the same backing array; changing an element through one is visible through the other. Nothing about the elements is copied. - **`copy(dst, src)`** copies elements between two existing windows, up to the shorter length. After it, the elements are independent - as long as the two slices did not already share an array. - **`append(dst, src...)`** may copy elements into spare capacity in place, or may allocate a fresh array and copy everything. Whether the caller's data is shared afterwards depends on capacity, which is why `append([]T(nil), src...)` - forcing an allocation by starting from nil - is the reliable way to spell "give me a detached copy". One more caution: all of these are shallow. `copy` of a `[][]byte` copies the inner slice headers, so the copies still point at the same inner arrays. If the element type contains a pointer, slice, map, or channel, the copy shares whatever it points to. ## What to say in an interview "`copy` returns `min(len(dst), len(src))` and only ever fills existing length - it ignores capacity, so a destination made with a zero length copies nothing. It handles overlap, so it is the right tool for sliding data inside a buffer, and `append` starting from nil, or `slices.Clone`, is how I ask for a detached copy."

  • How do you make a detached copy of a slice so writes to one are invisible to the other?
    `dst := append([]T(nil), src...)` or `slices.Clone(src)`; for bytes, `bytes.Clone(src)`. Starting from a nil slice forces an allocation, so the result never shares `src`'s array. Remember it is shallow: if `T` contains a pointer, slice or map, both copies still reference the same underlying data.
  • Why can copy handle overlapping source and destination when a naive loop cannot?
    The specification requires it to behave as if the source were read fully before the destination was written, and the implementation moves the memory in the direction that preserves that. A hand-written forward loop shifting elements left is fine, but shifting right overwrites values it has not read yet - copy removes that whole class of mistake.
  • Is copy(dst, src) a deep copy?
    No, it is always shallow: it copies element values. For `[]int` that is everything. For `[][]byte` it copies the inner headers, so both slices still point at the same inner arrays, and for a struct with a map field both copies share the map. A deep copy has to be written by hand or generated.

saying these in an interview costs you the question

  • Says copy fills the destination up to its capacity
  • Expects copy to allocate or grow the destination
  • Believes copy returns the copied slice
  • Thinks copy is undefined when the slices overlap
  • Calls copy a deep copy of the elements