Repeated append calls can force a Go slice to move to a larger backing array when its capacity runs out. If code already knows that several more elements are about to arrive, slices.Grow can reserve enough room before those appends happen.

The key detail is that slices.Grow changes capacity when needed, not length. Existing elements stay in place logically, and the returned slice still contains the same number of elements.

Reserve capacity with slices.Grow

slices.Grow(s, n) returns a slice with enough capacity for at least n more elements beyond its current length. Keep the returned value, just as you would with append:

package main

import (
    "fmt"
    "slices"
)

func main() {
    items := []int{10, 20}
    items = slices.Grow(items, 3)

    fmt.Println(len(items))
    fmt.Println(cap(items) >= len(items)+3)

    items = append(items, 30, 40, 50)
    fmt.Println(items)
}

The output confirms the guarantee:

2
true
[10 20 30 40 50]

After the call, the slice length is still 2. The function only makes sure there is room for three additional elements. The exact capacity is deliberately not part of the contract, so code should check or depend on the minimum guarantee rather than a particular capacity value.

slices.Grow does not add elements

A common mistake is treating capacity as if it were initialized slice length. This call:

items = slices.Grow(items, 100)

does not make len(items) equal to 100, and indexes such as items[50] are still out of range if the original length was smaller.

Use append when adding values:

items = slices.Grow(items, len(incoming))
items = append(items, incoming...)

Use make with a nonzero length when the program needs indexed slots immediately:

items := make([]int, 100)
items[50] = 7

Those operations express different intentions. slices.Grow reserves append room; make([]T, n) creates n addressable elements.

Grow only allocates when more room is needed

If the slice already has enough spare capacity, slices.Grow can return it without allocating a larger backing array.

For example:

items := make([]string, 2, 10)
items[0] = "alpha"
items[1] = "beta"

items = slices.Grow(items, 5)

The slice has eight unused slots before the call, so the requested five additional slots already fit. Its length remains 2 and its existing values remain unchanged.

This makes slices.Grow useful when the caller knows an upcoming append count but does not want to duplicate capacity arithmetic. The function handles both cases: enough capacity already exists, or more capacity must be obtained.

Reassign the returned slice

Always keep the value returned by slices.Grow:

items = slices.Grow(items, 20)

If more capacity is required, the returned slice may refer to different backing storage. Ignoring that return value means later code continues using the old slice header and does not get the capacity reservation.

This is the same practical rule used with append. A slice operation that may replace backing storage needs its returned slice assigned somewhere.

Use Grow when the extra count is known

Suppose a parser collects a header and then receives a batch whose size is already available:

func addBatch(dst []string, batch []string) []string {
    dst = slices.Grow(dst, len(batch))
    return append(dst, batch...)
}

The reservation guarantees that the following append of the whole batch has enough capacity. That can avoid an allocation during that append when dst was short on room.

There is still a trade-off. Reserving capacity consumes memory even if later control flow does not append all the expected elements. Calling Grow far ahead of actual use, especially with large estimates, can keep more memory available than the program needs.

If the final size is known when creating a fresh slice, make is often simpler:

results := make([]Result, 0, len(inputs))

slices.Grow is especially convenient when a slice already exists and code discovers the next batch size later.

Zero and negative growth requests behave differently

Passing zero is valid. It asks for no additional capacity and preserves the slice value, including nilness:

var items []int
items = slices.Grow(items, 0)

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

A negative request is invalid and causes a panic:

items = slices.Grow(items, -1)

An excessively large request that cannot be allocated also panics. If the requested count comes from untrusted or externally supplied data, validate its range before using it as a capacity request. That also prevents a malformed size field from driving an unreasonable memory reservation.

Capacity reservation is not a memory limit

slices.Grow guarantees a minimum amount of append room. It does not promise that capacity will equal len(s) + n, and it is not a tool for enforcing a maximum buffer size.

If code must reject data beyond a fixed limit, perform that limit check explicitly before growing or appending:

const maxItems = 10_000

if len(items) > maxItems-len(batch) {
    return nil, fmt.Errorf("item limit exceeded")
}

items = slices.Grow(items, len(batch))
items = append(items, batch...)

The subtraction form avoids relying on an addition that could overflow for extreme integer values. Capacity management and input limits solve separate problems, so keeping both rules visible makes the code easier to audit.

Reserve close to the append

Use slices.Grow when an existing slice is about to receive a known number of elements and reserving that room makes the append path more predictable. Keep the returned slice, remember that its length does not change, and avoid depending on an exact resulting capacity.

For fresh slices with a known target size, start with an appropriate make capacity. For existing slices that receive batches over time, place slices.Grow close to the append it prepares. That keeps the reservation tied to a concrete need instead of turning spare capacity into a guess made far from the code that uses it.