How to Sort a Map by Key or Value in Go

Go maps are unordered; extract keys or values into a slice and use slices.Sort to order them.

The map that refuses to stay in order

You write a Go program that tallies HTTP status codes from a log file. You dump the counts into a map. You print the map. The output looks random. You run it again. The order changes. You try to sort it by frequency, but Go gives you a flat refusal: maps are unordered by design. This is not a missing feature. It is a deliberate choice about how hash tables work under the hood.

Why Go maps skip sorting entirely

Go maps are backed by hash tables. Hash tables trade order for speed. They calculate a hash from the key, use that to find a bucket, and drop the value in. When you iterate, the runtime walks through buckets in an intentionally shuffled order to prevent programs from relying on accidental ordering. If you need order, you do not sort the map. You extract the data into a slice, sort the slice, and work with the slice. The map stays a fast lookup table. The slice becomes your ordered view.

Think of a map like a kitchen pantry. You throw spices in wherever there is space. You find them fast when you know what you want. You do not rearrange the pantry every time you want to read the labels alphabetically. You take the jars out, line them up on the counter, and sort them there. The pantry stays optimized for grabbing. The counter becomes your sorted workspace.

Go enforces this separation at the language level. A map type carries no ordering guarantee. The runtime explicitly randomizes iteration order to catch bugs where developers accidentally depend on insertion order. When you need a sorted view, you materialize one. You pay the allocation and comparison cost upfront, and you get a predictable sequence.

Maps are lookup tables. Slices are ordered sequences. Keep them in their lanes.

Sorting by key: the straightforward path

Extracting keys and sorting them is the most common pattern. Go 1.21 introduced the maps and slices packages to make this ergonomic. You pull the keys out, sort them in place, and iterate.

Here is the simplest key sort: extract the keys, sort the slice, and walk through it.

package main

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

func main() {
	// map holds unsorted key-value pairs
	m := map[string]int{"zebra": 3, "apple": 1, "mango": 2}

	// pull keys into a slice for sorting
	keys := slices.Collect(maps.Keys(m))
	// sort the slice in place using binary search tree comparisons
	slices.Sort(keys)

	// iterate in alphabetical order
	for _, k := range keys {
		fmt.Println(k, m[k])
	}
}

The maps.Keys function returns an iterator. slices.Collect consumes that iterator and builds a slice. The slice lives on the heap if it escapes, or on the stack if it stays local. slices.Sort runs a fast introsort algorithm. It swaps elements in place. No extra allocations happen during the sort itself.

When you iterate over the sorted keys, you still look up values in the original map. This keeps the sort cheap. You only move strings around in memory. The integer values stay put. If your keys are large structs, consider sorting pointers or indices instead. Copying heavy keys into a slice defeats the purpose of a lightweight sort.

Convention note: Go developers rarely write custom sort functions for simple types. The standard library provides optimized routines. Trust the built-in sort. Write custom comparators only when the ordering logic depends on business rules.

Keys sort fast. Values require pairing.

Sorting by value: pairing keys and values

Sorting by value requires keeping keys and values together. You cannot sort values in isolation and expect to know which key produced which value. You build a slice of structs, populate it, and sort the slice.

Here is the standard value sort: pair keys and values, sort by the value field, and read the ordered pairs.

package main

import (
	"fmt"
	"slices"
)

func main() {
	// map holds unsorted key-value pairs
	m := map[string]int{"zebra": 3, "apple": 1, "mango": 2}

	// preallocate slice to avoid reallocations during append
	type kv struct {
		key   string
		value int
	}
	pairs := make([]kv, 0, len(m))

	// populate the slice with every map entry
	for k, v := range m {
		pairs = append(pairs, kv{k, v})
	}

	// sort by value ascending, fall back to key for stability
	slices.SortFunc(pairs, func(a, b kv) int {
		if a.value != b.value {
			return a.value - b.value
		}
		return slices.Compare(a.key, b.key)
	})

	// print the ordered results
	for _, p := range pairs {
		fmt.Println(p.key, p.value)
	}
}

The struct pairs each key with its value. The slice grows to exactly the map size. slices.SortFunc takes a comparison function that returns a negative number if the first element comes first, zero if they are equal, and a positive number if the second comes first. The comparison function runs for every comparison during the sort. Keep it cheap.

Notice the fallback comparison on the key. slices.SortFunc is not stable by default. If two values are equal, their relative order after sorting is undefined. Adding a secondary comparison on the key guarantees deterministic output. Determinism matters for tests, caches, and reproducible builds.

Convention note: Receiver names in Go are usually one or two letters matching the type. If you turn this into a method, name the receiver m or kv, not this or self. The community expects short, predictable names. gofmt will enforce the spacing around the struct fields anyway. Let the formatter handle indentation. Focus your energy on the comparison logic.

Values sort by pairing. Stability requires a tiebreaker.

Where things go sideways

Sorting maps introduces a few common traps. The compiler catches type mismatches early. The runtime catches comparison bugs later.

If you pass the wrong type to slices.Sort, the compiler rejects the program with cannot use x (type T) as type []T in argument. The slices package is generic. It expects a slice. Passing a map directly triggers a hard error. You must extract first.

If your comparison function returns the wrong sign, the sort still runs, but the output looks wrong. The algorithm assumes a strict weak ordering. Returning zero for unequal elements breaks the contract. The compiler cannot check comparison logic at compile time. You get silent misordering. Write unit tests that assert exact output order.

If you sort a map that changes concurrently, you get a race condition. Maps are not safe for concurrent reads and writes. Extract the keys or pairs inside a critical section, or use a read-only snapshot. The compiler complains with concurrent map read and map write if the race detector is enabled. Fix the data flow before adding sorting logic.

If you use a.value - b.value for large integers, you risk overflow. Subtracting two large positive ints can wrap around to a negative number. The sort sees a negative result and places the larger value first. Use explicit comparisons instead: if a.value < b.value { return -1 } else if a.value > b.value { return 1 } else { return 0 }. Or use cmp.Compare from the standard library when available. The compiler will not warn you about arithmetic overflow in comparison functions. The bug hides until edge cases hit production.

Convention note: The if err != nil { return err } pattern is verbose by design. Sorting rarely returns errors, but if you wrap this in a function that validates input, keep the error handling explicit. Do not swallow validation failures. The unhappy path should be visible at the call site.

The worst sorting bug is the one that only appears with duplicate values.

Picking the right approach

Use a map when you need fast lookups by key and order does not matter. Use a sorted slice of keys when you only need to display or process keys in order and can afford a map lookup per iteration. Use a slice of key-value structs when you need to sort by value and must keep keys attached to their values. Use a stable sort with a secondary comparison when duplicate values must preserve a deterministic order. Use plain sequential code when the dataset is small enough that sorting overhead outweighs the benefit.

Maps optimize for retrieval. Slices optimize for sequence. Pick the container that matches the operation.

Where to go next