Sometimes you need only the keys from a Go map. A plain range loop handles that case well, but an iterator becomes useful when the keys need to flow into another iterator-aware API or when a function should expose keys without first allocating a slice.
Since Go 1.23, maps.Keys returns an iter.Seq over a map’s keys. You can range over it directly, stop early, or pass it to helpers such as slices.Sorted and slices.Collect.
maps.Keys returns an iterator, not a slice
The function has this shape:
func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]That return type matters. Code written for older helper libraries may expect a key slice, but the standard library function returns a sequence that yields one key at a time.
For direct iteration, no collection step is needed:
package main
import (
"fmt"
"maps"
)
func main() {
ports := map[string]int{
"api": 8080,
"admin": 9090,
"debug": 6060,
}
for name := range maps.Keys(ports) {
fmt.Println(name)
}
}This visits every key. The order is unspecified, just as it is with ordinary map iteration. Code must not depend on one observed ordering.
Stop iteration as soon as a key is enough
An iterator is especially convenient when the caller may stop before consuming every key. A break ends the range without requiring a temporary slice:
func findPort(m map[string]int, target int) (string, bool) {
for name := range maps.Keys(m) {
if m[name] == target {
return name, true
}
}
return "", false
}This example searches by value while exposing only keys from the iterator. For a single local loop, ranging over the map directly would be simpler. maps.Keys becomes more useful when the sequence itself is passed between functions or composed with iterator-aware helpers.
That distinction keeps the API choice practical: use the iterator when it fits the surrounding data flow, not merely to replace every for range over a map.
Sort map keys when output must be deterministic
Map iteration order is not specified and is not guaranteed to stay the same between calls. Logs, snapshots, generated text, and command output often need stable ordering, so sort the keys explicitly.
slices.Sorted accepts an iterator and returns a sorted slice:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
ports := map[string]int{
"db": 5432,
"api": 8080,
"cache": 6379,
}
for _, name := range slices.Sorted(maps.Keys(ports)) {
fmt.Printf("%s=%d\n", name, ports[name])
}
}The output is deterministic:
api=8080
cache=6379
db=5432Sorting necessarily materializes the keys because the sorting operation needs a collection it can reorder. If ordering has no observable effect, ranging over maps.Keys directly avoids that extra slice.
Collect keys only when another API needs a slice
Some APIs still accept []K rather than an iterator. slices.Collect converts the sequence into a slice:
keys := slices.Collect(maps.Keys(ports))The resulting slice contains all keys, but its order remains unspecified. Collection preserves the sequence it receives; it does not sort it.
If a stable slice is required, use slices.Sorted instead:
keys := slices.Sorted(maps.Keys(ports))This is clearer than collecting first and sorting in a separate statement when the only goal is a sorted key slice.
A nil map is safe here. It has no keys, so collecting maps.Keys from a nil map produces an empty slice. Direct iteration also performs zero iterations.
Avoid assumptions about mutation during iteration
maps.Keys follows the same map iteration semantics as ranging over a map. That includes the rules around changes made while iteration is in progress.
Deleting an entry that has not yet been reached guarantees that entry will not be produced. Adding an entry during iteration may produce the new key or may skip it. That behavior can vary from one iteration to another.
If the operation needs a fixed snapshot of the keys before mutation begins, collect them first:
keys := slices.Collect(maps.Keys(m))
for _, key := range keys {
// Changes to m do not change the already collected key slice.
}The slice captures the keys yielded during collection. Later map changes do not alter that slice, although referenced data inside a key type would still follow normal Go value semantics. In practice, map keys are comparable values, so the exact implications depend on the key type.
Prefer direct map range for the simplest local loop
maps.Keys is not a replacement for this:
for key := range m {
use(key)
}When all work stays inside one loop, direct map range is shorter and communicates the operation immediately. The iterator earns its place when it crosses an API boundary, feeds another iterator-aware function, or makes early consumption part of a reusable pipeline.
That keeps the code from adding abstraction without a benefit. Standard-library helpers are most useful when their return types fit the next operation naturally.
Use the sequence until a slice is actually required
Keep maps.Keys(m) as an iterator while the next operation can consume an iter.Seq. Range over it directly for streaming work, pass it to iterator-aware helpers, and materialize it only at a boundary that requires a slice.
When order matters, make that requirement explicit with sorting. When it does not, avoid turning unspecified map order into an accidental contract. Those two choices make maps.Keys straightforward to use without hiding allocation or ordering assumptions.