A slice assignment copies the slice header, not its backing array. That detail becomes visible as soon as one variable changes an element and another variable unexpectedly sees the same change. slices.Clone gives the copied slice separate element storage with a single call.

The copy is shallow. That makes it a clean fit for slices of numbers, strings, and value-only structs, but nested maps, slices, pointers, and other reference-like values still need deliberate handling.

Copy a Go slice with slices.Clone

Call slices.Clone and keep the returned slice:

package main

import (
    "fmt"
    "slices"
)

func main() {
    source := []int{4, 8, 15, 16}
    copied := slices.Clone(source)

    copied[0] = 99

    fmt.Println(source)
    fmt.Println(copied)
}

The output is:

[4 8 15 16]
[99 8 15 16]

Changing copied[0] doesn’t replace source[0] because the two slice values no longer use the same element storage.

This differs from plain assignment:

source := []int{4, 8, 15}
alias := source

alias[0] = 99

fmt.Println(source) // [99 8 15]

Here source and alias are separate slice headers that still refer to the same backing array.

Use slices.Clone at ownership boundaries

Cloning is useful when a function needs to retain or mutate a slice without changing storage owned by its caller. A constructor is a common example:

type Queue struct {
    items []string
}

func NewQueue(items []string) *Queue {
    return &Queue{items: slices.Clone(items)}
}

Without the clone, code holding the original slice could modify elements that the Queue considers its internal state. Copying at the boundary gives the struct independent element storage.

The same pattern works in the other direction. If a method returns an internal slice directly, callers can modify that storage. Returning a clone can keep the internal representation private:

func (q *Queue) Items() []string {
    return slices.Clone(q.items)
}

That protection has a cost: each returned copy needs storage proportional to the slice length. For hot paths or large slices, an iterator, indexed accessor, or documented read-only convention may be a better trade-off.

slices.Clone makes a shallow copy

Each element is copied by assignment. If an element itself contains a reference to mutable data, the clone still points at that nested data.

Consider a slice of structs containing another slice:

type Batch struct {
    Values []int
}

source := []Batch{
    {Values: []int{10, 20}},
}

copied := slices.Clone(source)
copied[0].Values[0] = 77

fmt.Println(source[0].Values) // [77 20]

The outer []Batch storage is separate, but source[0].Values and copied[0].Values still share the nested backing array.

If nested state must also be independent, copy that state explicitly:

func cloneBatches(source []Batch) []Batch {
    copied := slices.Clone(source)
    for i := range copied {
        copied[i].Values = slices.Clone(copied[i].Values)
    }
    return copied
}

There isn’t one universal deep-copy operation because element types can contain maps, pointers, channels, interfaces, or application-specific ownership rules. Copy the mutable layers that must be isolated.

Nil slices stay nil

slices.Clone preserves nilness:

var source []string
copied := slices.Clone(source)

fmt.Println(copied == nil) // true

That behavior can matter when an API distinguishes a nil slice from an allocated empty slice. For example, serialization or application logic may treat “not supplied” differently from “supplied with zero entries.”

An empty non-nil slice remains non-nil:

source := []string{}
copied := slices.Clone(source)

fmt.Println(copied == nil) // false
fmt.Println(len(copied))   // 0

No special guard is needed before cloning either form.

Don’t depend on cloned capacity

The returned slice has the same length as the input, but its spare capacity isn’t an API guarantee. Current documentation permits the clone to have additional unused capacity.

Code that needs a particular capacity should express that requirement separately instead of treating slices.Clone as a capacity-management tool. For example, if later appends need reserved room, clone first and then use slices.Grow:

copied := slices.Clone(source)
copied = slices.Grow(copied, 100)

Conversely, slices.Clip is the operation intended to reduce a slice’s capacity to its current length. Cloning and clipping solve different problems: one separates element storage, while the other changes the capacity exposed by a slice header.

Choose cloning for isolation, not by habit

Copying every slice defensively can create avoidable allocations and element-copy work. Clone when independent storage is part of the ownership contract: before mutating caller-owned data, when retaining data that callers may modify, or when returning internal storage would expose mutable state.

When sharing is intentional, a normal slice assignment is cheaper and communicates that no copy is required. When nested mutable values also need isolation, treat slices.Clone as the outer copy and add explicit copies for those nested values. That keeps both the cost and the ownership rules visible in the code.