Sunday, August 16, 2026
Go copy() vs append(): Which One Do You Actually Need?
Posted by

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:
append | copy | |
|---|---|---|
Grows len | yes (on the return value) | never |
| Allocates | when len+n > cap | never |
| Needs dest sized | no | yes |
| Typical job | build, concatenate | clone, 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.
Learn more: