Slice Header Copy & Append Surprise
Two functions share what looks like independent slices until one append overwrites the other's elements.
Search across all documentation pages
Two functions share what looks like independent slices until one append overwrites the other's elements.
This page shows how slice header copying creates aliasing, how append reuses capacity, and the copy patterns that prevent production data corruption.
A Go slice is a small header: pointer, length, and capacity.
Assignment and passing by value copy the header, not the underlying array.
When two headers point at the same array, append on one slice can mutate elements visible through the other if spare capacity exists.
These bugs are insidious because programs rarely panic - they return wrong data under specific sizes and load patterns.
Quick-reference recipe card - copy-paste ready.
// Limit capacity when exposing an interior view
sub := s[low:high:high] // cap == len, append reallocates
// Defensive copy before handing to caller
out := append([]T(nil), s...)
// Copy prefix only
prefix := append([]T(nil), s[:n]...)When to reach for this:
[]byte buffers reused across requestsappend after s[:n] on shared storage[]byte handling in HTTP/gRPC layerspackage main
import "fmt"
func main() {
base := []int{1, 2, 3, 4, 5}
window := base[1:3] // len 2, cap 4 (shares backing array)
fmt.Println("before", base, window, "cap", cap(window))
window = append(window, 99) // writes into base's backing array
fmt.Println("after ", base, window)
safe := append([]int(nil), base[1:3]...)
safe = append(safe, 99)
fmt.Println("safe ", base, safe)
}What this demonstrates:
window shares base's array; append with spare capacity mutates basecap(window) larger than len(window) is the danger signallen, cap, and full backing slice catches the defect in testspointer | len | caps[low:high] sets len=high-low, cap=cap(s)-low - often larger than lenappend reuses backing array when len < cap; otherwise allocates new array| Goal | Pattern |
|---|---|
| Expose read-only view | Return copy append([]T(nil), s...) |
| Interior view without append growth into parent | s[i:j:j] three-index slice |
| Truncate but keep cap | s = s[:n:n] before passing on |
| Reset reusable buffer | s = s[:0] only when sole owner |
// Parser keeping one []byte buffer
func parseFrame(buf []byte) (payload []byte, rest []byte) {
n := int(buf[0])
payload = append([]byte(nil), buf[1:1+n]...) // copy payload out
rest = buf[1+n:]
return payload, rest
}append always allocates - It reuses capacity when possible. Fix: check cap or copy first.s[:n] from internal buffer - Callers append into your pool. Fix: copy or three-index cap limit.bytes.Buffer.Bytes() aliasing - Bytes remain valid until next buffer mutation. Fix: copy if storing long-lived.s[low:high:high].s = nil before unmarshal if you need fresh allocation.| Alternative | Use When | Don't Use When |
|---|---|---|
| Three-index slice | Zero-copy view, no append by consumer | Consumer needs append headroom |
append([]T(nil), s...) | Handing ownership to caller | Hot path needs zero-copy and contract forbids append |
slices.Clip (Go 1.21+) | Trim cap to len in one call | Must support older Go without polyfill |
Separate []byte pool per goroutine | High-throughput parsers | Simple code paths where copy is cheap |
Slices are lightweight views.
Copying the array on every assignment would destroy performance.
Sharing is explicit via capacity rules.
Look for s[low:high] stored or returned, then append on the derived slice.
Check whether parent slice is still live.
Range over slice copies each element value.
Mutating struct fields via range value does not update the slice; index assignment does.
Strings are immutable.
Substrings share backing bytes but cannot be appended to.
slices.Clone(s) allocates a new slice with elements copied.
Use when you want stdlib helper instead of append([]T(nil), s...).
Yes - outer and inner slices both have headers.
Copy inner slices when handing rows to async work.
Mutate derived slice, assert parent unchanged.
Table-test with lengths where cap > len.
No - only capacity sharing affects elements within cap.
Parent len unchanged unless you assign back to parent variable.
Reader manages position but underlying slice may still alias.
Copy if consumers retain []byte views.
log.Printf("base=%p len=%d cap=%d %#v", &base[0], len(base), cap(base), base) before and after append on derived slice.
No - slice headers behave the same for []T with any T.
Hot parsers and network stacks.
Document that consumers must not append; enforce with three-index cap or opaque type.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026