A slice can have enough logical elements for the current operation and still lack room for the next append. When the number of upcoming elements is already known, slices.Grow makes that capacity requirement explicit without changing the slice length.
The function joined the standard library with the slices package in Go 1.21. Its contract is narrow: given a slice and a non-negative count, it returns a slice with enough capacity to append at least that many additional elements without another allocation.
Grow reserves capacity, not length
The generic signature preserves the input slice type:
func Grow[S ~[]E, E any](s S, n int) Sn describes additional append space. It does not describe the desired final length or final capacity.
package main
import (
"fmt"
"slices"
)
func main() {
values := []string{"api", "worker", "cron"}
values = slices.Grow(values, 4)
fmt.Println(len(values))
fmt.Println(cap(values) >= 7)
}The length remains 3. The returned slice has capacity for the existing three elements plus at least four more. The exact capacity is deliberately not part of the contract, so code should not depend on a particular growth factor.
That distinction matters when Grow sits near code that writes by index. Reserving capacity does not make values[3] valid. Only an operation that extends the length, such as append or reslicing within capacity, changes the set of valid indexes.
Existing spare capacity can satisfy the request
Grow only needs to allocate when the current backing array cannot satisfy the requested append space. A slice with length 3 and capacity 10 already has room for seven additional elements. Calling slices.Grow(s, 4) can therefore return a slice that still refers to the same backing array.
This means Grow is not a copying boundary. Code that needs storage independent from the input should use an operation with that contract, such as slices.Clone, rather than treating capacity growth as a clone.
The same point applies to aliases:
base := make([]int, 2, 8)
base[0], base[1] = 10, 20
alias := base
grown := slices.Grow(base, 3)
grown[0] = 99
fmt.Println(alias[0]) // 99The existing capacity already covers three more elements, so no new backing array is required. Both slice values can still observe writes to shared elements.
If allocation is required, the returned slice contains the original elements but may point at different storage. Callers should therefore keep the returned value, just as they do with append.
Reserving before a known append batch
The clearest use for Grow is a batch whose maximum or exact size is available before appending. Consider building a byte buffer from a header and payload:
func frame(header, payload []byte) []byte {
out := make([]byte, 0, len(header))
out = append(out, header...)
out = slices.Grow(out, len(payload))
out = append(out, payload...)
return out
}After Grow, the payload append has enough reserved capacity to complete without another allocation. The call does not promise that earlier operations avoided allocations, and it says nothing about appends beyond the reserved count.
This contract is more precise than guessing a capacity and more focused than changing length with make. It is especially useful when a slice already contains data and the next batch size becomes available later.
A direct allocation can still be simpler when the final size is known before the slice is created:
out := make([]byte, 0, len(header)+len(payload))Grow fits the case where capacity needs to be extended on an existing slice rather than established at construction.
Nil slices stay nil when no storage is needed
Current slices.Grow documentation specifies that the result preserves the nil state of the input. In particular, growing a nil slice by zero does not turn it into a non-nil empty slice:
var values []int
values = slices.Grow(values, 0)
fmt.Println(values == nil) // trueA positive request needs backing storage, so a successful call with a positive n can return a non-nil slice with zero length and positive capacity.
This distinction can matter in code where nil and non-nil empty slices carry separate serialization or API semantics. Capacity planning does not need to erase the nil state when no growth is requested.
Invalid growth requests panic
A negative n is a programmer error and causes Grow to panic. A request that is too large to allocate also panics.
values := []int{1, 2, 3}
values = slices.Grow(values, -1) // panicThat behavior makes Grow unsuitable as a direct validator for untrusted size fields. If an external request supplies a count, validate it against an application limit before converting it into a capacity reservation. Otherwise, an attacker-controlled value can drive an excessive allocation attempt even when the eventual append never occurs.
Capacity reservation is a memory decision. Keeping explicit bounds around externally derived sizes remains necessary.
The returned slice is the capacity contract
slices.Grow separates two concerns that are often mixed together in append-heavy code: the current logical length and the amount of storage reserved for upcoming elements. It guarantees append headroom while leaving length untouched, and it may reuse or replace the backing array to satisfy that guarantee.
The returned slice is therefore the value that carries the new capacity contract. Retaining it, while avoiding assumptions about exact capacity or allocation behavior, keeps the code aligned with what the standard library actually promises.