Copy a Slice in Go the Safe Way and Avoid Shared Memory
Assigning a slice copies only its header. Use slices.Clone or copy into a sized destination to get independent data, and watch the nil versus empty result.

You wrote b := a, changed b[0], and a[0] changed too. That is not a bug in Go. Assigning a slice copies its three word header and leaves the array where it is, which is the model laid out in slices from the header to the backing array. When you need a separate copy there are three idioms, and they differ in one small way that matters for JSON and for nil checks.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
The three ways
package main
import (
"fmt"
"slices"
)
func main() {
src := []int{1, 2, 3}
a := slices.Clone(src)
b := make([]int, len(src))
n := copy(b, src)
c := append([]int(nil), src...)
src[0] = 99
fmt.Println(src, a, b, c, n)
var wrong []int
fmt.Println(copy(wrong, src), wrong)
}[99 2 3] [1 2 3] [1 2 3] [1 2 3] 3
0 []All three copies keep 1 2 3 after the source was changed. slices.Clone is the shortest and the one I reach for when the toolchain is Go 1.21 or later. The make plus copy pair is the classic form, and it is the right one when you want a different capacity or want to copy into an existing buffer. The append onto a nil slice is the older idiom you will meet in code from before slices existed.
The last two lines show the mistake that wastes the most time. copy copies min(len(dst), len(src)) elements. A destination declared with var wrong []int has length zero, so copy returns 0 and the slice stays empty. Size the destination with make([]T, len(src)) first, with a length and not just a capacity. copy also handles overlapping slices correctly, which is how you shift elements within one slice.
Nil in, nil out, or not
The three forms treat an empty source differently:
package main
import (
"fmt"
"slices"
)
func main() {
var nilSlice []int
empty := []int{}
fmt.Println(slices.Clone(nilSlice) == nil, slices.Clone(empty) == nil)
fmt.Println(append([]int(nil), empty...) == nil)
fmt.Println(make([]int, len(nilSlice)) == nil)
}true false
true
falseslices.Clone preserves nil-ness: a nil input gives nil and an empty one gives a non-nil empty slice. The append form returns nil for any empty input, because appending nothing to nil gives nil. make always returns non-nil. This matters when the result is encoded as JSON, where nil becomes null and empty becomes [], and in any == nil check. If your API promises an empty array, use make or normalize after copying.
Shallow, not deep
All of these copy elements, not what the elements refer to. A []*User copied this way gives you a new slice of the same pointers, so changing copy[0].Name changes the original user. A [][]int is the same: the outer slice is new, the rows are shared. For a deep copy, loop and copy each level, or write a Clone method on the element type. This is the most common reason that a "copied" slice still changes when the original does.
Where copying is the wrong move
Do not copy defensively on every call. A function that only reads its argument does not need one, and a copy per request is real allocation. Copy where ownership changes hands: storing a caller's slice in a struct, returning an internal slice from a method, or retaining a small part of a large buffer. The aliasing trap that makes this matter is shown in the append trap that corrupts another slice, and removing an element without leaking it covers the other half of slice ownership. The official reference for the helper is slices.Clone, and the built-in is described in the specification.


