Sorting Structs in Go: The Short Way and Magic Under the Hood

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Sorting Structs in Go: How to Write Concisely Without Breaking Your Brain

\n\n

Imagine: 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.

\n\n

The Problem: “Why can’t you just write sort() in Go?”

\n\n

If you come from Python, JavaScript, or Ruby, you’re used to one line:

\n
users.sort(key=lambda u: u.age)
\n

Go 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:

\n
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}
\n

It 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\n

The Solution: sort.Slice — Your Swiss Army Knife

\n\n

Starting 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:

\n
sort.Slice(users, func(i, j int) bool {\n    return users[i].Age < users[j].Age\n})
\n

That’s it. Three lines. No declaring new types, no extra methods. Want to sort by name? Change the field:

\n
sort.Slice(users, func(i, j int) bool {\n    return users[i].Name < users[j].Name\n})
\n

And if you need descending order? Just change the sign:

\n
sort.Slice(users, func(i, j int) bool {\n    return users[i].Age > users[j].Age // now descending\n})
\n

Beautiful. But how does it work? And what’s the catch?

\n\n

Under the Hood: sort.Slice and Anonymous Functions

\n\n

Let’s break down what happens when you call sort.Slice. The function is declared like this:

\n
func Slice(slice interface{}, less func(i, j int) bool)
\n

The 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).

\n

Inside, sort.Slice does the following:

\n
    \n
  1. 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.
  2. \n
  3. Extracts the length via reflect.ValueOf(slice).Len().
  4. \n
  5. Creates a helper object that implements the sort.Interface dynamically. This object holds a reference to the original slice (via reflection) and to your less function.
  6. \n
  7. Runs the standard sorting algorithm (quicksort from the sort package). When the algorithm needs to compare two elements, it calls your less. When it needs to swap them, it uses reflect.Swapper(slice).
  8. \n
\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

Blogs

Book Recommendations