Sometimes you know a slice is about to receive several elements, even though you don’t have those elements yet. Repeated append calls will grow the slice automatically, but some of those appends may have to allocate a larger backing array and copy the existing elements.
slices.Grow lets you reserve enough capacity for a known amount of upcoming growth. It doesn’t add placeholder elements and it doesn’t change the slice’s length. It simply returns a slice that has room for at least the requested number of additional elements.
What slices.Grow guarantees
The function has this shape:
func Grow[S ~[]E, E any](s S, n int) SAfter slices.Grow(s, n), the returned slice has enough spare capacity for at least n more elements. In other words, this condition is guaranteed:
cap(grown) - len(grown) >= nThe length stays the same.
package main
import (
"fmt"
"slices"
)
func main() {
values := []int{10, 20}
values = slices.Grow(values, 3)
fmt.Println(len(values)) // 2
fmt.Println(values) // [10 20]
values = append(values, 30, 40, 50)
fmt.Println(values) // [10 20 30 40 50]
}The useful guarantee is about room for the next three appends, not the exact capacity chosen by the implementation. Code should not assume that cap(values) becomes exactly len(values)+3.
Grow capacity without inventing elements
A common alternative is to allocate a slice with the final length up front:
values := make([]int, 5)That is correct when all five positions are already part of the logical result and you’ll fill them by index. It is a poor substitute when only two elements exist and three more will be appended later. A length of five means the slice already contains five elements, with zero values occupying the unwritten positions.
slices.Grow keeps length and capacity as separate ideas:
values := []int{10, 20}
values = slices.Grow(values, 3)
// len(values) is still 2.
values = append(values, 30)This matters in code where len represents the number of completed records, encoded bytes, parsed tokens, or collected results. Reserving storage shouldn’t make unfinished elements appear to exist.
If you’re creating a new slice and already know a useful capacity, make remains straightforward:
values := make([]int, 0, expected)Grow is especially convenient when you already have a slice and discover later how much more data is coming.
Use slices.Grow when upcoming growth is known
Suppose a function has an existing batch and receives another batch whose size is known:
func addBatch(dst []string, batch []string) []string {
dst = slices.Grow(dst, len(batch))
for _, item := range batch {
dst = append(dst, item)
}
return dst
}The loop still uses ordinary append. Grow just establishes enough capacity before the loop starts.
For this exact example, append(dst, batch...) is simpler and should usually be preferred. The pattern becomes more useful when each input item conditionally produces output, when values require transformation before appending, or when several append operations are separated by other work.
For example:
func normalize(dst []string, names []string) []string {
dst = slices.Grow(dst, len(names))
for _, name := range names {
if name == "" {
continue
}
dst = append(dst, strings.ToLower(name))
}
return dst
}Here len(names) is an upper bound on the number of new elements. Reserving that much space may be reasonable if most names are retained. If almost every name is discarded and the input can be large, that estimate may reserve far more memory than the result needs.
Preallocation is a trade-off, not a default requirement
Go’s built-in append already handles slice growth. You don’t need to call slices.Grow before every append loop.
Preallocation is most defensible when the amount of upcoming data is known or can be estimated well and the code path benefits from avoiding intermediate growth. The trade-off is that reserved capacity occupies memory even if you never use it.
Consider a parser that receives a declared count of one million items but commonly rejects nearly all of them. Growing the result slice for one million elements before validation could create a large allocation for little benefit. In that case, incremental growth or a conservative estimate may be better.
The right question isn’t “Can I preallocate?” It’s “Do I have a trustworthy estimate that makes reserving this storage worthwhile?”
For performance-sensitive code, measure the real workload. Allocation behavior can matter, but an extra Grow call based on a poor estimate isn’t automatically an improvement.
Assign the returned slice
Like append, slices.Grow returns a slice. Keep that return value:
values = slices.Grow(values, additional)If the existing backing array already has enough spare capacity, the returned slice can continue using it. If not, Grow may allocate new storage and copy the existing elements there.
Ignoring the result is therefore a bug:
slices.Grow(values, additional) // result discardedThe original slice header doesn’t get magically updated to point at newly allocated storage. Any capacity guarantee belongs to the returned slice.
This is the same ownership habit that makes append reliable: once an operation may replace a slice’s backing array, use the returned slice from that point onward.
Existing aliases don’t gain the new capacity
Slices are descriptors over backing storage. If two slice values refer to the same array and one is passed to Grow, the other slice value is not updated.
values := []int{1, 2}
alias := values
values = slices.Grow(values, 100)
values = append(values, 3)If Grow had to allocate, values now refers to different storage while alias still refers to the original array. If no allocation was needed, they may still share storage.
Don’t use Grow as a way to deliberately detach a slice from aliases. Its contract is capacity, not copying. If you need an independent copy, use an operation whose purpose is copying, such as slices.Clone, and reason about growth separately.
Zero growth is valid, negative growth is not
Passing zero asks for room for zero additional elements. That requires no growth:
values = slices.Grow(values, 0)A nil slice also remains nil when no growth is requested:
var values []int
values = slices.Grow(values, 0)
fmt.Println(values == nil) // trueThe documentation guarantees that Grow preserves the nilness of its input. A positive request on a nil slice needs storage, so the resulting slice has capacity while its length remains zero.
A negative n is invalid and causes a panic. Requests that are too large to allocate also panic. That means a size derived from an external request, file, or protocol shouldn’t be passed to Grow without validation.
For example, avoid treating an untrusted count as an allocation instruction just because it parsed successfully. Apply application-specific bounds first, then convert that validated size into a capacity request.
Watch for integer arithmetic before calling Grow
The n argument is an int, so calculations used to produce it deserve the same overflow care as other allocation sizes.
Suppose each input record can produce up to three output entries. This looks natural:
additional := len(records) * 3
results = slices.Grow(results, additional)For realistic in-memory slices, len(records) is already constrained by addressable memory, but general allocation code should still avoid unchecked arithmetic when values can come from external sizes or multiple calculations. Validate bounds before multiplication or addition rather than relying on a later panic as normal error handling.
The capacity reservation should be the last step after deciding that the requested amount of work is acceptable.
Don’t depend on the exact capacity after Grow
slices.Grow promises enough capacity for n additional elements. It does not promise the smallest possible capacity or a particular growth factor.
This is brittle:
values = slices.Grow(values, 3)
if cap(values) != len(values)+3 {
// Wrong assumption about the implementation.
}If exact capacity is part of a test, the test is checking an implementation detail rather than the function’s contract. A better assertion is:
if cap(values)-len(values) < 3 {
t.Fatal("not enough spare capacity")
}The same rule applies to production logic. Use capacity as a property of storage, not as a hidden signal for application state.
Reserve storage where the estimate becomes reliable
The best place for slices.Grow is usually where code learns something concrete about upcoming output. That might be after decoding a bounded record count, after selecting a batch to transform, or immediately before a loop that will append a predictable number of results.
Don’t push the reservation far away from the append just to make a helper look optimized. Keeping the estimate near the operation makes its reasoning visible: readers can see why that amount was chosen and whether the estimate is exact, an upper bound, or merely a heuristic.
Use make([]T, 0, n) when constructing a new empty result with known capacity. Use ordinary append when growth is small or unpredictable. Use slices.Grow when an existing slice needs a known amount of additional room without changing its logical length.
That distinction is enough for most code. Reserve capacity when you have useful information, assign the returned slice, and leave the exact growth strategy to Go.