When append outgrows a Go slice's capacity, how does the runtime pick the new capacity?
answer
- reallocate and copy, never grow in place
- small ones double, big ones do not
- a threshold near a couple of hundred elements
- the allocator only sells fixed block sizes
- none of it is in the specification
basics
~20 sThe runtime allocates a larger array and copies into it. Small slices roughly double; past about 256 elements growth tapers toward 25 percent. The result is rounded up to an allocator size class, so the exact capacity is never guaranteed.
solid answer
~50 sWhen `len == cap`, append allocates a new backing array, copies the old elements, and returns a header pointing at it. The runtime starts from a rule of thumb: if the required capacity is more than double the old one, use the required capacity; otherwise double while the old capacity is under roughly 256 elements, and above that grow by a shrinking factor that trends toward about 1.25x. Whatever number that produces is then rounded **up** to one of the allocator's size classes, because the heap only hands out fixed block sizes. So `cap` after a growth is `>= len` and otherwise an implementation detail: it has changed between releases (the threshold was 1024 elements before Go 1.18) and is not in the language specification. If you need a specific capacity, ask for it with `make([]T, 0, n)`.
code
go · 7 linesvar s []int
for i := 0; i < 9; i++ {
s = append(s, i)
fmt.Println(len(s), cap(s))
}
// on a current 64-bit build: caps run 1, 2, 4, 4, 8, 8, 8, 8, 16
// not guaranteed by the language specificationgo deeper
Know that exceeding capacity means a new, larger array and a copy of everything already there, and that the growth is geometric rather than one element at a time.
Describe the policy: double while small, a tapering factor above a threshold of a few hundred elements, then rounding up to an allocator size class. Say clearly that it is not specified behaviour.
Turn it into practice: pre-size with make when the count is known, refuse exact-capacity assertions in tests, and reason about peak footprint when a very large slice regrows near a memory limit.
Own the guidance rather than the numbers. Decide when a hot path deserves an explicit capacity or a reused buffer, and keep the team from encoding a runtime detail into tests that a Go upgrade will then break.
## The trigger Every `append` first asks whether the destination has room: is `len(s) + <number of new elements> <= cap(s)`? If yes, the elements are written into the existing backing array and only the returned header's length changes. If no, the slice must move. The runtime allocates a new array, copies the old elements into it with a block copy, writes the new elements, and returns a header pointing at the new array. The old array is untouched and stays alive only as long as something still references it. ## Choosing the new capacity The policy the runtime uses today has three stages. 1. **Required capacity wins if it is large.** If you append many elements at once and the needed capacity is more than twice the old capacity, the runtime simply takes the needed capacity as the starting point. Growing geometrically past what you already asked for would be waste. 2. **Small slices double.** While the old capacity is below a threshold of roughly 256 elements, the new capacity starts at twice the old one. This is what makes a loop of one-at-a-time appends cheap: the number of copies is proportional to the final length, not to its square. 3. **Large slices taper.** Above that threshold, doubling would be violent - going from 100 MB to 200 MB to serve one more element. So the growth factor shrinks smoothly with size, trending toward roughly 1.25x per step. It is a formula, not a flat constant, precisely so that there is no cliff at the threshold. **Then the number is rounded up.** Go's allocator does not serve arbitrary byte counts; it serves size classes (8, 16, 32, 48, 64, 80 bytes and so on, page-based above 32 KB). The runtime converts the requested element count into bytes, rounds that up to the enclosing size class, and converts back to an element count. A `[]byte` appended to from nil typically lands on capacity 8, because 8 bytes is the smallest class. That rounding is why observed capacities look tidy for common element sizes and untidy for odd ones - a slice of a 24-byte struct will not give you powers of two. ## Watch it happen ``` var s []int for i := 0; i < 9; i++ { s = append(s, i) fmt.Println(len(s), cap(s)) } ``` On a current 64-bit build the capacities run 1, 2, 4, 4, 8, 8, 8, 8, 16. Useful to see once - and useful to *distrust*. It is not a guarantee. ## The part that matters more than the numbers **None of this is in the language specification.** The spec says only that append returns a slice with the new elements and that it may allocate. The threshold moved from 1024 elements to about 256 in Go 1.18, along with the smoothing that removed the discontinuity at the boundary. Any code, test or benchmark that asserts an exact `cap` after a growth is asserting an implementation detail, and a reviewer should push back on it. Asserting `cap(s) >= len(s)`, or that a capacity did **not** change across a call, is fine and is often the clearest way to prove no reallocation happened. ## What to do with the knowledge The practical consequences are narrow and worth stating precisely: - **Pre-size when you know the size.** `make([]T, 0, n)` before a loop that appends `n` items removes every intermediate allocation and copy. This is the single highest-value use of the knowledge. - **Do not pre-size on a guess.** Over-allocating a long-lived slice wastes memory that the runtime will not give back: slices never shrink their capacity on their own. - **Expect capacity to overshoot.** After growth, `cap(s)` is usually strictly greater than `len(s)`, which is exactly the spare room that later makes two slices over the same array write on top of each other. Growth policy and aliasing are the same subject seen from two sides. - **Do not confuse growth with the copy cost.** Each growth copies every element. For a slice of pointers or structs containing pointers the copy is a plain memory move, but the new array must also be scanned by the collector, so a very large slice that keeps regrowing costs both bandwidth and mark work. ## What to say in an interview "It reallocates and copies. Roughly doubling while small, tapering toward about 1.25x once it is past a couple of hundred elements, then rounded up to an allocator size class. The exact number is an implementation detail that has changed between releases - if I need a capacity I make it explicitly."
- Why does the growth factor taper instead of doubling all the way up?Doubling a large slice asks the allocator for a block twice the size of one that is already big, to serve possibly one element, and the copy touches every byte. Tapering toward roughly 1.25x keeps the amortised cost acceptable while capping the worst-case overshoot and peak footprint, which for a multi-hundred-megabyte buffer is the difference between fitting in memory and not.
- You append 1000 elements to an empty slice in a loop. How would you remove the intermediate allocations?Create it pre-sized: `s := make([]T, 0, 1000)`, then append as normal. Length stays 0 so the appends still work naturally, and no growth ever fires. Note `make([]T, 1000)` is different - it gives you 1000 zero values, and appending after that puts elements at index 1000.
- Is it reasonable for a unit test to assert an exact capacity after append?No, if it is asserting the growth policy - that number is an implementation detail that has already changed between Go releases. It is reasonable to assert a relationship: that capacity is unchanged across a call (proving nothing reallocated), or that `cap >= len`. In review, an exact-capacity assertion is a comment worth leaving.
saying these in an interview costs you the question
- States Go always doubles capacity at every growth
- Claims a fixed 1.5x growth factor like other languages
- Says append grows the existing array in place
- Believes the capacity sequence is guaranteed by the spec
- Thinks removing elements shrinks a slice's capacity