---
title: How to Use the slices and maps Packages with Iterators
summary: Use slices.Sorted and maps.Keys to iterate over ordered collections and key-value pairs efficiently in Go.
lang: en
slug: how-to-use-the-slices-and-maps-packages-with-iterators
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.gofaq.org/en/how-to-use-the-slices-and-maps-packages-with-iterators/
---

# How to Use the slices and maps Packages with Iterators



## The problem with temporary slices

You have a map of configuration headers that needs to be logged in alphabetical order. You have a slice of user IDs that needs to be filtered, deduplicated, and passed to a downstream service. The traditional Go approach requires building a temporary slice, sorting it, looping over it, and discarding it when the scope ends. You end up writing boilerplate just to move data from one shape to another. The allocations add up. The index tracking gets noisy. The code reads like a chore instead of a pipeline.

Go 1.23 introduced iterators to the standard library. The `slices` and `maps` packages now expose lazy sequences that integrate directly with `range`. You stop managing indices. You stop allocating intermediate collections. You describe what you want, and the runtime delivers it one value at a time.

## What an iterator actually is

An iterator in Go is a function that yields values on demand. It does not build a new slice or map in memory. It computes the next value only when the consumer asks for it. The type signature looks like this: `iter.Seq[T] func(yield func(T) bool) bool`. The outer function returns a boolean indicating whether the sequence is complete. The inner `yield` function is called by the iterator to hand a value to the consumer. If the consumer returns `false`, the iterator stops early. If it returns `true`, the iterator continues.

Think of it like a water tap instead of a bucket. A traditional loop fills a bucket first, then drinks from it. An iterator turns on the tap, drinks a glass, turns it off, and repeats. The water never sits idle in memory. The pattern scales cleanly to maps with `iter.Seq2[K, V]`, which yields a key and a value together.

Iterators are lazy. They compute on demand, not upfront.

## The minimal pattern

Here is the simplest way to iterate over a map in sorted order and a slice in sorted order using the standard library.

```go
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	// maps.Keys returns an iterator over the map keys
	// slices.Sorted consumes it and yields keys in ascending order
	for k := range slices.Sorted(maps.Keys(config)) {
		fmt.Println(k, config[k])
	}

	// slices.Values turns the slice into an iterator
	// slices.Sorted consumes it and yields values in ascending order
	for v := range slices.Sorted(slices.Values(numbers)) {
		fmt.Println(v)
	}
}

var config = map[string]string{
	"host": "localhost",
	"port": "8080",
	"env":  "dev",
}

var numbers = []int{42, 7, 19, 3}
```

The `range` keyword now accepts `iter.Seq[T]` and `iter.Seq2[K, V]` directly. You do not need to convert the iterator to a slice first. The loop drives the sequence automatically.

## How the runtime drives the sequence

When the compiler sees `for v := range slices.Sorted(slices.Values(numbers))`, it generates a loop that calls the outer iterator function once. That function returns a `yield` callback and a `done` boolean. The runtime stores the callback and enters a loop. On each iteration, it calls `yield` with the next value. The iterator's internal state advances. When the iterator has no more values, it sets `done` to `true` and returns. The loop exits.

No intermediate slice is allocated. The memory footprint stays flat regardless of input size. The `slices.Sorted` function collects the values into a temporary slice only when it needs to sort them, because sorting requires random access. Functions like `slices.Values`, `maps.Keys`, and `maps.Values` are purely streaming. They never allocate.

The loop drives the iterator. The iterator never decides when to stop.

## Chaining in a real pipeline

Real code rarely iterates over a single collection. You filter, transform, deduplicate, and pass the result downstream. Iterators compose cleanly because each function returns another iterator. You chain them like pipes.

Here is a realistic example that processes a slice of transaction records. It filters out incomplete transactions, extracts the amounts, sorts them, and passes the sequence to a reporting function.

```go
package main

import (
	"fmt"
	"slices"
)

// ReportTotals consumes an iterator of amounts and prints the sum
func ReportTotals(amounts iter.Seq[int]) {
	sum := 0
	for a := range amounts {
		sum += a
	}
	fmt.Println("total:", sum)
}

func main() {
	// raw transactions from a database or API
	transactions := []struct {
		ID       int
		Amount   int
		Complete bool
	}{
		{1, 100, true},
		{2, 50, false},
		{3, 200, true},
		{4, 75, true},
	}

	// slices.Values turns the struct slice into an iterator
	// slices.Filter keeps only completed transactions
	// slices.Map extracts the Amount field
	// slices.Sorted orders the amounts for deterministic reporting
	ReportTotals(
		slices.Sorted(
			slices.Map(
				slices.Filter(
					slices.Values(transactions),
					func(t struct{ ID, Amount int; Complete bool }) bool {
						return t.Complete
					},
				),
				func(t struct{ ID, Amount int; Complete bool }) int {
					return t.Amount
				},
			),
		),
	)
}
```

The chain reads left to right in terms of data flow, even though the code nests right to left. Each step is lazy. `slices.Filter` does not build a new slice. It hands values to `slices.Map` one at a time. `slices.Map` transforms them and hands them to `slices.Sorted`. `slices.Sorted` buffers only what it needs to sort, then yields. The reporting function consumes the stream without ever seeing the original struct slice.

Chain iterators like pipes. Let the data flow, not the allocations.

## Where iterators trip you up

Iterators change the mental model of loops. A few patterns cause friction until you adjust.

The first trap is expecting eager evaluation. If you assign an iterator to a variable but never `range` over it, nothing happens. The sequence is a description, not a result. You must drive it with a loop or `iter.Pull`.

The second trap is passing the wrong type to a function. Functions that accept iterators expect `iter.Seq[T]`, not `[]T`. If you pass a slice directly, the compiler rejects the program with `cannot use mySlice (variable of type []string) as iter.Seq[string] value in argument`. Wrap it with `slices.Values(mySlice)` to convert it.

The third trap is ignoring the `yield` return value. The `yield` function returns `bool`. `true` means continue. `false` means the consumer wants to stop early. If your custom iterator ignores this flag, it will keep running after the loop breaks, wasting CPU cycles. Always check the return value and return immediately when it is `false`.

Lazy means nothing happens until you range. Call the sequence or lose the data.

## Convention asides

The Go community treats iterators as plumbing, not magic. You will see `gofmt` handle the new syntax without complaint. The formatter aligns nested iterator chains cleanly, so you do not need to argue about indentation. Most editors run `gofmt` on save, and the output stays consistent across teams.

When you write a custom iterator, name the `yield` parameter `yield` and the outer function `yieldSeq` or `seq`. Keep the signature close to `iter.Seq[T]`. If you need to discard a value from a `Seq2`, use `_` for the unused variable. `for k, _ := range maps.All(m)` signals that you intentionally dropped the value. The underscore is a deliberate choice, not an accident.

Public iterator helpers start with a capital letter. Private ones start lowercase. The standard library follows this strictly. `slices.Values` is public. Internal helpers in your package stay lowercase. Accept interfaces, return structs. Iterators are functions, so they fit the pattern naturally.

## When to reach for iterators

Iterators solve a specific class of problems. They are not a replacement for every loop. Pick the right tool based on the shape of your data and the constraints of your code.

Use a traditional `for i, v := range slice` loop when you need direct index access or simple iteration over a small collection.
Use `slices.Values` or `maps.Keys` when you want to pass a collection to a function that expects an iterator.
Use `slices.Filter`, `slices.Map`, or `maps.All` when you need to chain transformations without allocating intermediate slices.
Use `slices.Sorted` or `maps.SortedKeys` when you need deterministic ordering without mutating the original data.
Use `iter.Pull` when you need to manually drive the sequence, such as in a custom parser or when integrating with C APIs that require explicit next calls.
Stick to plain loops when performance profiling shows iterator overhead matters in a tight inner loop.

Loops are the foundation. Iterators are the plumbing.

## Where to go next

- [How to Create Lazy Sequences in Go with Iterators](/en/how-to-create-lazy-sequences-in-go-with-iterators/)
- [How to Implement the Iterator Pattern in Go](/en/how-to-implement-the-iterator-pattern-in-go/)
- [Iterator Patterns: Map, Filter, Take, Skip in Go](/en/iterator-patterns-map-filter-take-skip-in-go/)