Batching a slice often starts as index arithmetic: advance by a fixed width, clamp the final boundary, and pass each sub-slice onward. Go 1.23 added slices.Chunk, which expresses that operation as an iterator while preserving the backing storage of the source slice.
Its behavior has one detail that matters beyond syntax: every yielded chunk has its capacity clipped to its length. A caller can modify elements through a chunk, but a plain append cannot grow that chunk into the next region of the source slice.
Chunk returns slice views through an iterator
The function accepts a slice and a positive chunk size:
func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]It returns an iter.Seq that yields consecutive sub-slices containing at most n elements. Every chunk except possibly the final one has length n.
package main
import (
"fmt"
"slices"
)
func main() {
values := []int{10, 20, 30, 40, 50}
for part := range slices.Chunk(values, 2) {
fmt.Println(part)
}
}The output is:
[10 20]
[30 40]
[50]Chunk does not require constructing a [][]int before iteration. The sequence produces each sub-slice as the range loop requests it.
The chunks still alias the source
A yielded chunk is a view into the original slice’s backing array, not a copy. Element updates through that view are therefore visible through the source slice.
values := []int{10, 20, 30, 40}
for part := range slices.Chunk(values, 2) {
part[0] = -1
}
fmt.Println(values)This prints:
[-1 20 -1 40]That property makes Chunk suitable when a consumer can operate directly on portions of an existing buffer. It also means a chunk should not be treated as detached data. If independent ownership is required, clone the yielded slice before retaining or mutating it independently.
for part := range slices.Chunk(values, 2) {
owned := slices.Clone(part)
consume(owned)
}The clone has separate element storage, so later writes through owned do not change values.
Capacity clipping prevents cross-chunk growth
Ordinary sub-slicing can expose spare capacity after the visible elements. Given a source slice, an expression such as s[0:2] may have enough capacity for append to write into elements that sit beyond that view.
Chunk deliberately removes that possibility. Each yielded sub-slice is clipped so that:
cap(part) == len(part)For example:
values := []int{1, 2, 3, 4}
for part := range slices.Chunk(values, 2) {
fmt.Println(len(part), cap(part))
}Both chunks report length 2 and capacity 2.
If code appends another value to one of these chunks, append cannot reuse the source array beyond the chunk boundary. It must obtain storage with additional capacity.
values := []int{1, 2, 3, 4}
for part := range slices.Chunk(values, 2) {
grown := append(part, 99)
fmt.Println(grown)
}
fmt.Println(values)The original elements remain:
[1 2 99]
[3 4 99]
[1 2 3 4]Element assignment within a chunk still aliases the source. Capacity clipping changes growth behavior; it does not turn the chunk into a copy.
Empty input produces no chunk
An empty source yields an empty sequence. There is no single empty slice emitted to represent the input.
count := 0
for range slices.Chunk([]int{}, 3) {
count++
}
fmt.Println(count)The result is 0. Code that expects at least one batch must handle the empty-input case separately.
The chunk size also has a strict boundary. n must be at least 1. Passing zero or a negative value causes a panic. When the size comes from configuration or external input, validation belongs before the call.
Early iteration avoids visiting later chunks
Because the result is an iterator, a consumer can stop without traversing the rest of the slice.
for part := range slices.Chunk(values, 100) {
if acceptable(part) {
process(part)
break
}
}No later chunk is yielded after the break. This differs from first building a complete outer slice of chunk descriptors and then ranging over it. Chunk keeps the partitioning operation in the iteration path.
The source storage still has to remain valid for the period in which the sequence is consumed. As with other slice views, retaining a small chunk can also retain the backing array that contains the full source. Copying is appropriate when a retained chunk should have independent storage or when keeping the larger array reachable is undesirable.
Chunk size defines boundaries, not parallelism
slices.Chunk partitions a sequence of elements. It does not schedule work, create goroutines, limit concurrent operations, or copy data into isolated batches.
That distinction matters when chunks feed concurrent code. Separate chunks refer to non-overlapping element ranges, but they still originate from the same backing array. Concurrent access is safe only when the actual reads and writes satisfy Go’s normal synchronization rules. Shared state outside the chunks is unaffected by the partitioning.
The function is best viewed as a boundary generator over a slice. It removes repeated index calculations and gives each boundary a capacity limit that blocks accidental append growth into its neighbor. Copying, ownership, retention, and concurrency remain decisions for the code consuming those boundaries.