A Go slice can be much smaller than the backing array it refers to. That spare capacity is useful when more append calls are coming, but sometimes you deliberately want the opposite: a result whose capacity stops exactly at its current length.

slices.Clip expresses that operation directly. It reduces a slice’s capacity to its length without changing its elements or length. The detail that matters is what this does, and doesn’t, imply about memory.

What slices.Clip changes

The function has this signature:

func Clip[S ~[]E, E any](s S) S

For a non-empty slice, the returned slice contains the same elements and has the same length, while its capacity equals that length.

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := make([]int, 3, 10)
    copy(numbers, []int{10, 20, 30})

    numbers = slices.Clip(numbers)

    fmt.Println(numbers)      // [10 20 30]
    fmt.Println(len(numbers)) // 3
    fmt.Println(cap(numbers)) // 3
}

Before the call, numbers has length 3 and capacity 10. Afterwards, both are 3. No elements were removed.

This is the same capacity restriction historically written with a full slice expression:

numbers = numbers[:len(numbers):len(numbers)]

slices.Clip(numbers) makes the intent easier to recognize and avoids repeating the length expression.

Clipping does not copy the slice

A tempting interpretation is that slices.Clip allocates a right-sized backing array. It doesn’t promise that. Conceptually, clipping restricts the slice header’s capacity; the returned slice can still refer to the same backing array as the input.

That distinction matters if your goal is to release a large backing allocation. Consider a small subslice of a large buffer:

buffer := make([]byte, 1<<20)
small := buffer[:16]
small = slices.Clip(small)

cap(small) is now 16, but clipping alone isn’t a guarantee that the megabyte backing array can be reclaimed while small remains reachable. If ownership of a small independent result is what you need, make a copy instead:

small := slices.Clone(buffer[:16])

Use Clip to control capacity. Use copying when the requirement is independent storage or allowing the original backing array to become unreachable.

Why clipping changes the next append

The practical effect of slices.Clip becomes visible when you append. Once capacity equals length, there is no spare slot available through the clipped slice, so appending an element requires the result to use additional storage.

original := make([]int, 3, 8)
copy(original, []int{1, 2, 3})

clipped := slices.Clip(original)
clipped = append(clipped, 4)

fmt.Println(original) // [1 2 3]
fmt.Println(clipped)  // [1 2 3 4]

This can be useful at an ownership boundary. Suppose a helper returns a slice backed by a work buffer with spare capacity. Clipping the returned view prevents a caller’s next append from quietly using that spare portion of the same array.

There is a caveat: clipping doesn’t stop writes to elements that are already in the slice. If two slices share the first three elements, assigning clipped[0] = 99 can still be visible through the other slice. Clip changes capacity, not element aliasing.

Clip after shrinking when spare capacity is unwanted

Operations that shorten a slice often leave capacity larger than length. Filtering is a common example:

jobs = slices.DeleteFunc(jobs, func(job Job) bool {
    return job.Done
})

If jobs started with substantial capacity and the filtered result is intended to be a finished collection, you can make its capacity boundary explicit:

jobs = slices.Clip(jobs)

This isn’t something to apply mechanically after every deletion. Spare capacity is beneficial when the slice will grow again. Clipping immediately before a series of appends can force growth sooner and work against the reason slices have capacity in the first place.

A useful rule is to clip when the capacity boundary itself serves a purpose: you’re handing a slice to another part of the program, you’ve finished building it, or you specifically don’t want future appends to consume the existing spare capacity.

Nil and empty slices keep useful distinctions

Clipping a nil slice returns a nil slice:

var values []string
values = slices.Clip(values)
fmt.Println(values == nil) // true

A non-nil empty slice remains non-nil, although its capacity is reduced to zero:

values := make([]string, 0, 8)
values = slices.Clip(values)

fmt.Println(values == nil) // false
fmt.Println(cap(values))   // 0

That distinction can matter in code that intentionally treats nil and non-nil empty slices differently, such as particular serialization or API conventions.

Don’t use slices.Clip as a memory-release shortcut

The most common mistake is reading cap(s) == len(s) as proof that only that much backing storage exists. Capacity describes how far the slice may extend through its current view; it isn’t a measurement of the allocation behind the slice.

If retaining a large backing array is the actual problem, create an independent copy of the elements you intend to keep and drop references to the original array. If preventing an append from reusing spare capacity is the problem, slices.Clip is the precise tool.

Those goals can look similar in a capacity printout, but they have different memory and aliasing behavior.

Use Clip when the capacity boundary is intentional

slices.Clip is a small operation with a narrow contract: preserve the slice’s contents and length while reducing capacity to length. That’s useful when a slice is finished growing or when you want the next append to cross a storage boundary rather than reuse hidden spare capacity.

Keep the limitation in view. Clipping isn’t cloning, and it doesn’t promise a smaller backing allocation. When you need independent ownership, copy. When you need len(s) == cap(s), clip.