44.3 Go 1.21: slices, maps, and cmp Packages; log/slog; min/max Built-ins
Alright, let’s get our hands dirty with Go 1.21. This release wasn’t about reinventing the wheel; it was about finally putting air in the tires and giving you a proper spare. We’re talking about quality-of-life improvements so good you’ll wonder how we ever lived without them. The designers finally looked at all the boilerplate we’d been writing for a decade and said, “Yeah, we can fix that.”
The slices and maps Packages: Your New Best Friends
For years, if you wanted to do anything mildly interesting with a slice or map—sorting, comparing, finding an element—you had to either write a clunky sort.Interface implementation, loop until your eyes bled, or pull in some random third-party library. No more. The slices package is a treasure trove of generic functions that do what you actually mean.
Want to check if two slices are equal, even if they’re in different parts of memory? slices.Equal is your huckleberry. It compares the lengths and then each element. And before you ask, yes, it works with your custom types because of generics. Magic.
package main
import (
"fmt"
"slices"
)
func main() {
teamA := []string{"Alice", "Bob", "Charlie"}
teamB := []string{"Alice", "Bob", "Charlie"}
teamC := []string{"Bob", "Charlie", "Alice"}
fmt.Println(slices.Equal(teamA, teamB)) // true: same order, same values
fmt.Println(slices.Equal(teamA, teamC)) // false: different order
// But what if order doesn't matter? Sort first!
slices.Sort(teamA)
slices.Sort(teamC)
fmt.Println(slices.Equal(teamA, teamC)) // true
}
The maps package brings the same energy. maps.Clone is a godsend. It gives you a shallow copy of a map, so you can stop doing the awkward dance of making a new map and looping over the old one. maps.Equal does for maps what slices.Equal does for slices, but it’s smarter—it knows map iteration is random, so it doesn’t care about order.
userRoles := map[string]string{
"alice": "admin",
"bob": "editor",
}
// Finally, a clean way to clone a map!
clonedRoles := maps.Clone(userRoles)
// And see if two maps have the same key-value pairs.
fmt.Println(maps.Equal(userRoles, clonedRoles)) // true
Pitfall: Remember, these are shallow copies and comparisons. If your values are pointers, slices, or other maps, you’re copying and comparing the pointer, not the data it points to. Deep copying is still on you, I’m afraid. Some things never change.
cmp: The Comparison Cornerstone
This is a tiny package with a massive impact. It defines the Ordered constraint (all the basic types you can compare with <, <=, >=, >) and provides cmp.Compare and cmp.Less. Why should you care? Because it standardizes comparison logic. Before this, every generic function that needed to compare two values defined its own constraint. It was a mess. Now, we have one common way to do it.
import "cmp"
func Score[T cmp.Ordered](a, b T) int {
// cmp.Compare returns -1 if a < b, 0 if a == b, 1 if a > b.
// It's the three-way comparison you didn't know you needed.
return cmp.Compare(a, b)
}
func main() {
fmt.Println(Score(42, 42)) // 0
fmt.Println(Score(3.14, 2.71)) // 1
fmt.Println(Score("apple", "banana")) // -1
}
This is the unsexy plumbing that makes the slices package so powerful. slices.Sort uses cmp.Less under the hood for any type that qualifies.
log/slog: Structured Logging, At Last
Let’s be honest: Go’s previous logging story was… primitive. log.Println is fine for a 10-line script, but for anything serious, you immediately reached for zerolog or zap. The core team finally conceded we were right and gave us slog, a structured logging package in the standard library.
The beauty of slog is its two-level API. The simple level for when you just want to get going, and the handler level for when you need serious control.
package main
import (
"log/slog"
"os"
)
func main() {
// Quick and easy, outputs: time=2023-... level=INFO msg="user logged in" id=1234
slog.Info("user logged in", "id", 1234)
// But let's do it properly.
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("user logged in",
slog.Int("id", 1234),
slog.String("email", "alice@example.com"),
)
// Outputs: {"time":"...","level":"INFO","msg":"user logged in","id":1234,"email":"alice@example.com"}
}
Best Practice: Use the slog.Type attribute keys (slog.Int, slog.String) instead of the bare "key", value style. It’s more verbose but far safer—you avoid the runtime panic if the key isn’t a string, and it’s more explicit and easier to refactor.
min and max: The Built-ins You Didn’t See Coming
This is the most delightfully absurd addition. The language designers added built-in functions min and max that work on any type that supports comparison. No more writing if a < b { return a } else { return b }. It’s a small thing, but it removes a tiny, persistent papercut.
x, y := 10, 20
fmt.Println(min(x, y)) // 10
fmt.Println(max(x, y)) // 20
// And yes, it works with more than two arguments. Because why not?
fmt.Println(min(9, 5, 7, 3, 1)) // 1
Edge Case: Here’s the fun part. Because they are built-ins, not functions from a package, they shadow any other functions you might have in scope called min or max. If you have a function func min(a, b int) in your package, it will be overshadowed by the built-in. The compiler will kindly warn you about this. It’s a bizarre little quirk, but one you’re unlikely to run into often. Just something to be aware of if your code suddenly starts behaving strangely.