Back to blog

Sunday, August 16, 2026

Go copy() vs append(): Which One Do You Actually Need?

Title card for 'Go copy() vs append(): Which One Do You Actually Need?' with a short code example

They are not interchangeable. append adds elements and may allocate; you must keep the returned header. copy overwrites existing slots in dst from src and returns how many it wrote. It never changes len or cap. Use copy to clone or to move bytes into a buffer you already have. Use append to grow. copy into a nil slice copies zero elements.

The failure

This looks like a clone. dst stays empty.

package main

import "fmt"

func main() {
    src := []int{1, 2, 3}
    var dst []int
    copy(dst, src)
    fmt.Println(dst, len(dst)) // [] 0
}

copy respects len(dst), not cap(dst). A nil slice has length 0.

The other mix-up: using append when you wanted an independent duplicate, then mutating through a shared array, or using copy when you meant to grow:

s := []int{1, 2, 3}
copy(s, []int{4, 5, 6, 7})
fmt.Println(s) // [4 5 6]: 7 never fits; copy does not extend s

Why they differ

append(s, x) builds a new header. If cap has room it writes past the old len on the same array. If not, it allocates. Either way the new len only exists on the value it returns.

copy(dst, src) is a memory move. Destination length is the ceiling. Overlap is defined: shifting left with copy(s, s[1:]) is safe. It still does not isolate two headers if they already shared an array and you copy in place.

Both use the same kind of bulk copy under the hood. The difference is the contract:

appendcopy
Grows lenyes (on the return value)never
Allocateswhen len+n > capnever
Needs dest sizednoyes
Typical jobbuild, concatenateclone, fill a buffer, shift in place

Concatenating with dst = append(dst, src...) is one allocation if dst has no spare room. A make + two copys is the same work, usually harder to read. Prefer append for concat unless you already allocated the exact destination.

Cloning with append([]T(nil), src...) works but often over-allocates capacity. make([]T, len(src)); copy(...) gives cap == len. From Go 1.21, slices.Clone(src) is the same idea with a name.

The fix

Clone, independent array, exact size:

src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
dst[0] = 9
fmt.Println(src) // [1 2 3]

Grow or concatenate:

a := []int{1, 2}
b := []int{3, 4}
a = append(a, b...)

Fill a pre-sized buffer (length already set):

buf := make([]byte, 8)
n := copy(buf, "hello")
fmt.Println(n, string(buf[:n])) // 5 hello

If you will append after a clone, either clone then append, or append onto a nil slice from the start. Do not copy into a destination whose length is 0 and expect growth.