Sometimes a Go map’s keys are irrelevant to the next operation. You may need to total counters, inspect status values, or pass the values into an iterator-aware helper. A plain map range works well inside one loop, but it doesn’t give you a value sequence that can cross an API boundary.

Since Go 1.23, maps.Values returns an iter.Seq over a map’s values. That lets code consume values directly and postpone slice allocation until a later operation actually needs a slice.

maps.Values returns a value iterator

The function has this shape:

func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V]

The result is an iterator rather than []V. You can range over it without building an intermediate collection:

package main

import (
    "fmt"
    "maps"
)

func main() {
    latency := map[string]int{
        "api":    42,
        "worker": 67,
        "cron":   31,
    }

    total := 0
    for ms := range maps.Values(latency) {
        total += ms
    }

    fmt.Println(total)
}

This prints 140. The loop receives each value once for that iteration, while the keys stay out of the loop entirely.

For a single local loop, for _, value := range m is just as direct and avoids introducing another function call in the source. maps.Values is more useful when the sequence itself feeds another operation or is returned from a helper.

Map value order remains unspecified

maps.Values does not impose an order on the map. Its documentation states that iteration order is unspecified and is not guaranteed to remain the same from one call to the next.

That matters whenever output order is visible. Consider collecting values:

values := slices.Collect(maps.Values(scores))

The resulting slice contains the yielded values, but code must not assume a stable position for any value. Running the same operation again can produce a different order.

If deterministic ordering is required and the value type is ordered, sort as part of materialization:

package main

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

func main() {
    scores := map[string]int{
        "api":    82,
        "worker": 91,
        "cron":   76,
    }

    values := slices.Sorted(maps.Values(scores))
    fmt.Println(values)
}

The output is:

[76 82 91]

slices.Sorted consumes the iterator and returns a sorted slice. This is useful for deterministic reports or tests when the association between each key and value is no longer needed.

Keep keys when the association still matters

Discarding keys is only correct when each value stands on its own. If two services both report 82, a value-only iterator cannot tell you which service produced either occurrence.

For work that needs the pair, use ordinary map range or maps.All instead:

for name, score := range maps.All(scores) {
    fmt.Printf("%s=%d\n", name, score)
}

This distinction is easy to miss when a pipeline starts with values but later needs context for logging, filtering, or error messages. Once maps.Values has removed the keys from the sequence, downstream code cannot reconstruct the original associations in general.

Repeated values are still yielded separately. A map with three different keys mapped to 0 produces three 0 values during a complete iteration.

Stop without collecting the rest

Because maps.Values returns an iterator, a range loop can stop as soon as the caller has enough information:

func hasNegative(m map[string]int) bool {
    for value := range maps.Values(m) {
        if value < 0 {
            return true
        }
    }
    return false
}

No temporary value slice is required. This can also make a helper’s interface fit other iterator-based code without forcing callers to materialize every value first.

Still, don’t choose maps.Values merely for early exit. Direct map range also supports break and return. The iterator is most useful when it composes naturally with the rest of the API.

Nil maps produce no values

A nil map has no entries, so ranging over maps.Values performs zero iterations:

var counts map[string]int

for value := range maps.Values(counts) {
    fmt.Println(value)
}

Nothing is printed.

There is a small detail worth preserving in tests: with Go 1.23, collecting that sequence with slices.Collect produces a nil slice:

values := slices.Collect(maps.Values(counts))
// values == nil

Both nil and non-nil empty slices have length zero, but they can differ in code that explicitly checks nilness or in some serialization boundaries. If only len(values) matters, both represent zero collected values.

Mutation follows normal map iteration rules

maps.Values uses map iteration, so changing the map while consuming the iterator carries the same caveats as a regular range over a map.

If an entry that has not yet been reached is deleted, its value will not be produced. An entry added during iteration may be produced or may be skipped. Code that needs a fixed set of values before mutations begin should collect first:

snapshot := slices.Collect(maps.Values(m))

for _, value := range snapshot {
    // Later changes to m do not change which values are in snapshot.
}

This snapshots the map values by ordinary assignment. If the value type contains pointers, slices, maps, or other reference-bearing data, collecting does not deep-copy the referenced data. Mutating that underlying data can still be visible through values stored in the snapshot.

Use maps.Values at iterator boundaries

Use maps.Values when the next operation needs values as an iter.Seq, especially when that keeps the data streaming until a later boundary. Keep direct map range for short local loops where an iterator adds no practical benefit.

Materialize with slices.Collect only when a slice is required, and use an explicit sorting operation when deterministic order matters. If downstream code needs to know which key produced a value, keep the pair instead of discarding information too early.