1. Stable output and map
If you’re just getting started, it can feel like output order is just aesthetics: “well, it printed all the elements—who cares what order they’re in.” The problem is that order suddenly becomes important in two cases: when you compare results visually (“what changed?”) and when results are compared programmatically (automated tests, scripts, grading systems).
Imagine you’re writing a small utility that prints stats: how many times each word occurred, which tags are unique, which tasks are marked as completed. You run the program twice with the same input—and you expect the same text. If the line order “floats,” both humans and tests start getting annoyed. And annoyed tests are like an anxious cat: it might knock everything off your desk just because “it feels like it today.”
map prints in a “random” order
When we do:
for k, v := range m {
fmt.Println(k, v)
}
we get a correct traversal: every “key → value” pair will be visited exactly once (as long as you don’t modify the map during iteration). But the iteration order is not part of the contract. That means you can’t write logic like “the first key is the smallest” or “the last key is the newest.” A map in Go has no notion of “first” or “last.”
There’s an important practical nuance here: even if today it “randomly” prints in a nicely sorted order, that’s not a promise. Tomorrow it’s a different run, a different environment, a different version—and the order changes. In official Go compatibility materials, there’s a specific example where a change in the sorting implementation between versions changed the order of equal elements, breaking programs that “expected a specific output.”
So we take away a simple rule: if the order matters for a human or for tests, you must define the order explicitly.
Basic recipe: “keys → sort → print”
The most convenient and simple way to “order” a map sounds almost like a cooking recipe—except instead of a pot, you use a slice. You do this: collect all keys into a separate slice, sort that slice, then print values strictly in the order of the sorted keys. In other words, you don’t sort the map (you can’t “sort” it directly); you sort the keys slice.
To keep the picture in your head, you can imagine it like this:
flowchart TD
A[map: key -> value] --> B["collected keys into []K"]
B --> C["sorted []K"]
C --> D["for _, k := range keys: print k and m[k]"]
Now—on to the code. We’ll start with a small scaffold for our example: a mini program that counts word frequencies (a frequency dictionary). This is handy because map[string]int demonstrates both a “counter” use case and the need for stable output.
2. Sorting simple keys with the sort package
When the keys are strings or numbers, the easiest approach is to use the sort package. It’s old and reliable—like a grandpa who grumbles but always shows up to fix the faucet.
Example: printing a map[string]int in alphabetical order
We count words and print frequencies. First we’ll show unstable output (how you shouldn’t do it), then we’ll fix it.
package main
import "fmt"
func main() {
counts := map[string]int{"go": 3, "map": 1, "sort": 2}
for w, c := range counts {
fmt.Println(w, c) // order is not guaranteed
}
}
Now we make the output stable: collect keys, sort them, print.
package main
import (
"fmt"
"sort"
)
func main() {
counts := map[string]int{"go": 3, "map": 1, "sort": 2}
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, counts[k]) // go 3 / map 1 / sort 2 (always the same)
}
}
The make([]string, 0, len(counts)) part is especially useful: we pre-allocate capacity for all keys up front and avoid making append grow the buffer extra times. This isn’t “optimization for optimization’s sake,” just good hygiene.
Example: if the keys are numbers (sort.Ints)
Sometimes the key is not a string but, for example, an ID. The recipe is exactly the same; only the key type and the sorting function change.
package main
import (
"fmt"
"sort"
)
func main() {
m := map[int]string{10: "ten", 2: "two", 7: "seven"}
keys := make([]int, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
fmt.Println(k, m[k]) // 2 two / 7 seven / 10 ten
}
}
An important idea: stability here comes from the fact that keys are unique. If keys are unique, even an “unstable sort” still produces the same result, because there are no equal elements that could be swapped.
3. Sorting by a rule: sort.Slice
Very quickly, you’ll want more than just “alphabetical order”—you’ll want “by meaning.” For example: print words by decreasing frequency, and if frequencies are equal, by alphabetical order. Or print strings by length. Or print numbers so that evens come first, then odds.
For these cases, the sort package has an “all-in-one” tool: sort.Slice. It sorts any slice if you provide a comparison rule as a function less(i, j int) bool.
Under the hood, the idea is simple: “the element at index i should come before the element at index j if less(i, j) == true.”
Example: sort strings by length and add a tie-breaker
package main
import (
"fmt"
"sort"
)
func main() {
words := []string{"go", "gopher", "map", "slices"}
sort.Slice(words, func(i, j int) bool {
if len(words[i]) != len(words[j]) {
return len(words[i]) < len(words[j])
}
return words[i] < words[j] // tie-breaker for determinism
})
fmt.Println(words) // [go map gopher slices]
}
Notice the second branch: return words[i] < words[j]. This is the tie-breaker (a rule for “equal” cases). It’s crucial if you want truly deterministic output.
Why does it matter? Because different versions and different sorting implementations can rearrange elements that are considered equal by your primary criterion. In Go compatibility materials, it’s explicitly emphasized that changes in the sorting implementation can change the order of equal elements, and you must not rely on a “random order among equals.”
Example: sort map keys by their values
Let’s go back to our map[string]int counter. We want to print the most frequent word first. Since you can’t sort a map, we sort the keys but compare them using the values in the map.
package main
import (
"fmt"
"sort"
)
func main() {
counts := map[string]int{"go": 3, "map": 1, "sort": 3}
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
wi, wj := keys[i], keys[j]
if counts[wi] != counts[wj] {
return counts[wi] > counts[wj] // higher comes first
}
return wi < wj // if frequencies are equal, order alphabetically
})
for _, k := range keys {
fmt.Println(k, counts[k])
}
}
Notice how carefully we implement the tie-breaker: if frequencies are equal, we still define the order via wi < wj. That makes the output “rock-solid”: same input → same output.
4. The slices package: Sort and SortFunc
Starting with Go 1.21, the standard library includes the slices package, which provides many convenient operations on slices, including sorting, and it often reads more cleanly than combinations with sort.Interface. Release notes also highlight that slices includes sorting functions and generally makes slice work more convenient and “ergonomic.”
Today we’re interested in two functions: slices.Sort (when elements are “orderable”—numbers, strings) and slices.SortFunc (when you need a custom order).
slices.Sort: straightforward sorting of strings and numbers
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{5, 2, 10, 2}
slices.Sort(nums)
fmt.Println(nums) // [2 2 5 10]
}
Important to remember: slices.Sort sorts in place and returns nothing. That’s a normal model for sorting: permuting elements doesn’t change slice length, so returning a new slice isn’t required.
By the way, the slices package also includes functions that return a new slice (for example, deleting a range). A common beginner mistake there is to ignore the returned value. In Go materials, these kinds of pitfalls are discussed: slices.Sort(s) correctly returns nothing, but slices.Delete returns a new slice, and you must not ignore the result.
slices.SortFunc: sorting with a comparator
sort.Slice asks for less(i, j) bool, while slices.SortFunc asks for an element comparator: cmp(a, b T) int.
The contract is usually:
- if a should come before b, return a negative number;
- if after, return a positive number;
- if they are “equal” from the sort’s perspective, return 0.
Let’s sort strings by length as before, but using slices.SortFunc:
package main
import (
"fmt"
"slices"
)
func main() {
words := []string{"go", "gopher", "map", "slices"}
slices.SortFunc(words, func(a, b string) int {
if len(a) != len(b) {
return len(a) - len(b)
}
if a < b {
return -1
}
if a > b {
return 1
}
return 0
})
fmt.Println(words) // [go map gopher slices]
}
Yes, it’s a bit more verbose than we’d like, but the comparison logic now works directly with the elements a and b, without indexes. For beginners, this often reads more easily: you compare values, not “elements at index i.”
And again, notice: a tie-breaker is required if you want a deterministic result. Return zero only when the strings are truly identical; otherwise, it’s better to resolve the order yourself via a tie-breaker.
5. Example: a frequency dictionary with stable output
Now we’ll put everything together into a small program that reads words from stdin, counts frequencies, and prints the result in a stable form. We’ll implement two output modes: alphabetical and by frequency. This isn’t a “full application,” but it’s real practice: you’ll see how a map turns into a neat, predictable report.
Reading words and counting
package main
import "fmt"
func main() {
counts := make(map[string]int)
for {
var w string
_, err := fmt.Scan(&w)
if err != nil {
break
}
counts[w]++
}
fmt.Println("unique:", len(counts)) // e.g.: unique: 3
}
Here we use fmt.Scan in a loop “until an error.” When input ends (EOF), Scan returns an error and we exit the loop. We won’t dive into error kinds here; the fact “there is an error → we’re done reading” is enough.
Stable alphabetical output (sort.Strings)
package main
import (
"fmt"
"sort"
)
func printAlpha(counts map[string]int) {
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, counts[k])
}
}
This function is pleasant to call: it doesn’t mutate the map, it just prints. And it always prints the same way.
Stable output by frequency (sort.Slice)
package main
import (
"fmt"
"sort"
)
func printByCount(counts map[string]int) {
keys := make([]string, 0, len(counts))
for k := range counts {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
a, b := keys[i], keys[j]
if counts[a] != counts[b] {
return counts[a] > counts[b]
}
return a < b
})
for _, k := range keys {
fmt.Println(k, counts[k])
}
}
Even if two words occur the same number of times, we still print them in a consistent order (alphabetical). This is a small detail, but it’s exactly such details that make a program “predictable.”
Call both outputs from main
package main
import "fmt"
func main() {
counts := map[string]int{"go": 3, "map": 1, "sort": 3}
fmt.Println("== alphabet ==")
printAlpha(counts)
fmt.Println("== by count ==")
printByCount(counts)
}
6. Common mistakes with stable output and sorting
Mistake #1: “I sorted the keys, but I print by iterating the map with range.”
It looks like this: you do sort.Strings(keys), and then you accidentally write for k, v := range m { ... }. As a result, your keys are sorted, but they aren’t used at all, and the order “dances” again. The correct idea is simple: the sorted source of order is the keys slice, so you must print strictly with for _, k := range keys.
Mistake #2: no tie-breaker when sorting by a “secondary” criterion.
For example, you sort words by frequency and return counts[a] > counts[b], and if frequencies are equal you return false. Then equal elements may be rearranged arbitrarily, and the output becomes unstable. This is especially nasty because it “almost always works,” and then suddenly starts flickering in tests. Changes in the sorting implementation between Go versions can change the order of equal elements, and you can’t rely on that.
Mistake #3: expecting sorting to return a new slice and continuing to use the old one.
Sorting functions in sort and slices modify the slice in place. So if you wanted to “preserve the original order,” make a copy first (for example, via append([]T(nil), s...)), and sort the copy. Also keep in mind that the slices package has functions that modify in place (for example, slices.Sort), and functions that return a new slice (for example, slices.Delete), and ignoring the result in the second case is a classic trap.
Mistake #4: trying to “sort a map.”
Sometimes a beginner searches for something like sort.Map(m) or tries to convert a map into a “sorted map.” In Go, there’s no built-in concept like that: a map is a structure for fast key lookup, not for storing elements in order. If you need order, you define it on top of the map: keys in a slice, sort, print.
Mistake #5: the comparator in slices.SortFunc returns “whatever.”
If the comparison function sometimes returns 1, sometimes -1 for the same values (or violates the common-sense rule “if a < b, then b > a”), sorting may behave strangely. Stick to the simple contract: negative means “before,” positive means “after,” zero means “equal.” And try to return 0 only when elements are truly equal; otherwise, it’s better to resolve ordering with a tie-breaker.
GO TO FULL VERSION