Sorting Structs in Go: How to Write Concisely Without Breaking Your Brain
\n\nImagine: you have a slice of structs, and you need to sort it by any field. Name, age, salary, date of birth — anything you want. On Stack Overflow, the question “What is the shortest way to simply sort an array of structs by (arbitrary) field names?” has gathered thousands of views. And this is no coincidence. Go doesn’t have a built-in “magic” method like list.sort(key=lambda x: x.field) from Python. But there is an elegant and powerful way — sort.Slice. In this article, we won’t just show the code; we’ll look under the hood: how sorting works, what the runtime does, and how to avoid common pitfalls.
The Problem: “Why can’t you just write sort() in Go?”
\n\nIf you come from Python, JavaScript, or Ruby, you’re used to one line:
\nusers.sort(key=lambda u: u.age)\nGo doesn’t have such luxury. Strong typing and the absence of generics (before Go 1.18) forced you to write a separate sort.Interface implementation for each struct type. This was cumbersome. For example:
type ByAge []User\nfunc (a ByAge) Len() int { return len(a) }\nfunc (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }\n\nfunc main() {\n users := []User{{"Alice", 30}, {"Bob", 25}}\n sort.Sort(ByAge(users))\n}\nIt works, but for each field — a new type. What if you have 10 fields? The code turns into a mess. You want something more compact.
\n\nThe Solution: sort.Slice — Your Swiss Army Knife
\n\nStarting with Go 1.8, the sort package introduced the Slice function. It takes a slice and an anonymous function (closure) that defines the order. Here it is — the shortest way:
sort.Slice(users, func(i, j int) bool {\n return users[i].Age < users[j].Age\n})\nThat’s it. Three lines. No declaring new types, no extra methods. Want to sort by name? Change the field:
\nsort.Slice(users, func(i, j int) bool {\n return users[i].Name < users[j].Name\n})\nAnd if you need descending order? Just change the sign:
\nsort.Slice(users, func(i, j int) bool {\n return users[i].Age > users[j].Age // now descending\n})\nBeautiful. But how does it work? And what’s the catch?
\n\nUnder the Hood: sort.Slice and Anonymous Functions
\n\nLet’s break down what happens when you call sort.Slice. The function is declared like this:
func Slice(slice interface{}, less func(i, j int) bool)\nThe first thing that stands out is interface{}. The function accepts any slice. How does it know its length and how does it swap elements? The answer — through reflection (the reflect package).
Inside, sort.Slice does the following:
- \n
- Checks that a slice was passed. If you pass something that isn’t a slice (e.g., an array or struct), the program will panic at runtime. \n
- Extracts the length via
reflect.ValueOf(slice).Len(). \n - Creates a helper object that implements the
sort.Interfacedynamically. This object holds a reference to the original slice (via reflection) and to yourlessfunction. \n - Runs the standard sorting algorithm (quicksort from the
sortpackage). When the algorithm needs to compare two elements, it calls yourless. When it needs to swap them, it usesreflect.Swapper(slice). \n
The key point: your less function captures the original slice from the outer context. This is a closure. The compiler places the users variable on the heap so that it remains accessible even after the function m