Splitting a slice into bounded groups often starts as index arithmetic: advance by a fixed width, clamp the final boundary, and take a sub-slice. Go 1.23 puts that operation in the standard library as slices.Chunk.

Chunk returns an iterator rather than a [][]T. That detail keeps grouping separate from collecting, and its capacity rule gives each yielded group a useful boundary for later append calls.

Chunk produces consecutive sub-slices

The signature is:

func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]

Each yielded value covers the next n elements of s, except the final value, which may be shorter. An empty input produces no values. A group size below one causes a panic.

package main

import (
	"fmt"
	"slices"
)

func main() {
	values := []string{"a", "b", "c", "d", "e"}

	for group := range slices.Chunk(values, 2) {
		fmt.Println(group)
	}
}

The output is:

[a b]
[c d]
[e]

The iterator form matters when the caller only needs one group at a time. There is no requirement to construct an outer slice containing every group before iteration begins.

The groups share element storage

A chunk is a sub-slice of the input, not a copy of its elements. Updating an element through a yielded group therefore updates the corresponding element in the original slice.

values := []int{10, 20, 30, 40}

for group := range slices.Chunk(values, 2) {
	group[0] = 0
}

fmt.Println(values)
// [0 20 0 40]

This follows ordinary Go slice semantics: multiple slices can describe regions of the same backing array. Code that needs independent ownership must copy each group before retaining or modifying it independently.

The shared storage also means retaining a small group can retain the backing array that contains the source data. For long-lived storage, a copy can make the ownership boundary explicit.

Capacity is clipped at each group boundary

Chunk does more than calculate start and end indexes. Every yielded sub-slice has its capacity clipped to its length.

That prevents an append from extending a group directly into the following group’s region of the backing array.

Consider a hand-written sub-slice:

values := []int{1, 2, 3, 4}
group := values[:2]

group = append(group, 99)
fmt.Println(values)
// [1 2 99 4]

Because group can still have spare capacity in the original backing array, the append may overwrite the next element.

A group from slices.Chunk has cap(group) == len(group). Appending another element therefore requires separate backing storage for the grown result.

values := []int{1, 2, 3, 4}

for group := range slices.Chunk(values, 2) {
	fmt.Println(len(group), cap(group))
	group = append(group, 99)
	fmt.Println(group)
}

fmt.Println(values)
// [1 2 3 4]

Element assignment still reaches shared storage; capacity clipping does not make the group immutable or independent. It specifically prevents growth within the original array past the group’s end.

Iterator control stays with the caller

Because Chunk returns iter.Seq, a range loop can stop without materializing the remaining groups.

for group := range slices.Chunk(records, 100) {
	if !accept(group) {
		break
	}
	process(group)
}

This shape also composes with APIs that consume standard iterator sequences. If an outer slice is actually required, slices.Collect can materialize the sequence:

groups := slices.Collect(slices.Chunk(values, 2))

The resulting outer slice stores slice descriptors. The group elements still refer to the source backing storage unless they are copied separately.

Group size is a contract, not a hint

A positive n sets the maximum group length exactly. Every group except the final one has length n. The final group contains the remaining elements.

That makes Chunk suitable for operations whose API accepts bounded batches, but it does not add concurrency, rate control, retries, or independent allocation. Those concerns remain with the surrounding code.

Passing zero or a negative value is a programmer error and causes a panic. When the group size comes from configuration or external input, validation belongs before the call.

slices.Chunk is a small API with precise slice semantics: consecutive views, lazy iteration, and capacity capped at each boundary. The last property is easy to miss, but it makes grouped slices safer to append to without silently consuming elements assigned to the next group.