Go Sort Package: Custom Sorting
Go's standard library `sort` package provides efficient sorting algorithms out of the box. While `sort.Strings()`, `sort.Ints()`, and `sort.Float64s()` handle basic types, real-world applications...
Key Insights
- Go’s
sort.Interfacerequires implementing three methods (Len, Less, Swap), butsort.Sliceoffers a more convenient alternative for most use cases with inline comparison functions - Multi-field sorting requires careful ordering of comparison logic—evaluate primary criteria first, then secondary criteria only when primary values are equal
sort.SliceStablepreserves the original order of equal elements and has identical performance tosort.Slice, making it the safer default choice for most applications
Introduction to Go’s sort Package
Go’s standard library sort package provides efficient sorting algorithms out of the box. While sort.Strings(), sort.Ints(), and sort.Float64s() handle basic types, real-world applications demand custom sorting logic for structs, complex comparisons, and multi-field ordering.
The traditional approach requires implementing the sort.Interface:
type Interface interface {
Len() int // returns the number of elements
Less(i, j int) bool // reports whether element i should sort before element j
Swap(i, j int) // swaps elements at positions i and j
}
Here’s the difference between basic and custom sorting:
// Basic sorting - works out of the box
names := []string{"Charlie", "Alice", "Bob"}
sort.Strings(names)
fmt.Println(names) // [Alice Bob Charlie]
// Custom sorting - requires implementation
type Person struct {
Name string
Age int
}
people := []Person{
{"Charlie", 35},
{"Alice", 28},
{"Bob", 42},
}
// Need custom logic to sort by age or name
Implementing sort.Interface for Custom Types
The traditional method involves creating a custom type and implementing all three interface methods. This approach provides maximum control and was the only option before Go 1.8.
package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func main() {
people := []Person{
{"Charlie", 35},
{"Alice", 28},
{"Bob", 42},
}
sort.Sort(ByAge(people))
for _, p := range people {
fmt.Printf("%s: %d\n", p.Name, p.Age)
}
// Output:
// Alice: 28
// Charlie: 35
// Bob: 42
}
This approach requires defining a new type (ByAge) for each sorting criterion. While verbose, it’s useful when you need to reuse sorting logic across your codebase or when building sortable collections as part of your API.
Using sort.Slice for Inline Sorting
Go 1.8 introduced sort.Slice() and sort.SliceStable(), dramatically simplifying custom sorting. These functions accept a comparison function, eliminating the need for type definitions.
package main
import (
"fmt"
"sort"
)
type Person struct {
Name string
Age int
}
func main() {
people := []Person{
{"Charlie", 35},
{"Alice", 28},
{"Bob", 42},
}
// Sort by name
sort.Slice(people, func(i, j int) bool {
return people[i].Name < people[j].Name
})
for _, p := range people {
fmt.Printf("%s: %d\n", p.Name, p.Age)
}
// Output:
// Alice: 28
// Bob: 42
// Charlie: 35
}
The comparison function receives indices and returns true if element i should come before element j. This inline approach is more readable and requires significantly less boilerplate.
Multi-Field Sorting and Complex Comparisons
Real applications often require sorting by multiple fields—for example, grouping employees by department and then ordering by salary within each group.
package main
import (
"fmt"
"sort"
)
type Employee struct {
Name string
Department string
Salary int
}
func main() {
employees := []Employee{
{"Alice", "Engineering", 120000},
{"Bob", "Sales", 80000},
{"Charlie", "Engineering", 95000},
{"Diana", "Sales", 90000},
{"Eve", "Engineering", 110000},
}
// Sort by department (ascending), then salary (descending)
sort.Slice(employees, func(i, j int) bool {
if employees[i].Department != employees[j].Department {
return employees[i].Department < employees[j].Department
}
// Within same department, higher salary comes first
return employees[i].Salary > employees[j].Salary
})
for _, e := range employees {
fmt.Printf("%s - %s: $%d\n", e.Department, e.Name, e.Salary)
}
// Output:
// Engineering - Alice: $120000
// Engineering - Eve: $110000
// Engineering - Charlie: $95000
// Sales - Diana: $90000
// Sales - Bob: $80000
}
The key pattern: check the primary criterion first. Only evaluate secondary criteria when primary values are equal. This cascading approach extends to any number of fields.
Sorting with Custom Comparison Logic
Beyond simple field comparisons, you often need custom logic like case-insensitive sorting, reverse ordering, or business-rule-based comparisons.
package main
import (
"fmt"
"sort"
"strings"
)
// Case-insensitive string sorting
func sortCaseInsensitive() {
words := []string{"Zebra", "apple", "Banana", "cherry"}
sort.Slice(words, func(i, j int) bool {
return strings.ToLower(words[i]) < strings.ToLower(words[j])
})
fmt.Println(words) // [apple Banana cherry Zebra]
}
// Custom business logic: discounted items first, then by price
type Product struct {
Name string
Price float64
Discounted bool
}
func sortProducts() {
products := []Product{
{"Widget", 29.99, false},
{"Gadget", 19.99, true},
{"Gizmo", 39.99, false},
{"Doohickey", 24.99, true},
}
sort.Slice(products, func(i, j int) bool {
// Discounted items come first
if products[i].Discounted != products[j].Discounted {
return products[i].Discounted
}
// Within same discount status, sort by price ascending
return products[i].Price < products[j].Price
})
for _, p := range products {
discount := ""
if p.Discounted {
discount = " (SALE)"
}
fmt.Printf("%s: $%.2f%s\n", p.Name, p.Price, discount)
}
// Output:
// Gadget: $19.99 (SALE)
// Doohickey: $24.99 (SALE)
// Widget: $29.99
// Gizmo: $39.99
}
For reverse sorting, simply invert the comparison: return a[i] > a[j] instead of return a[i] < a[j].
Performance Considerations
Both sort.Interface and sort.Slice use the same underlying algorithm (introsort: quicksort with heapsort fallback). Performance differences are minimal in most cases.
package main
import (
"sort"
"testing"
)
type Person struct {
Name string
Age int
}
type ByAge []Person
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func BenchmarkSortInterface(b *testing.B) {
people := make([]Person, 1000)
for i := range people {
people[i] = Person{Age: 1000 - i}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
data := make([]Person, len(people))
copy(data, people)
sort.Sort(ByAge(data))
}
}
func BenchmarkSortSlice(b *testing.B) {
people := make([]Person, 1000)
for i := range people {
people[i] = Person{Age: 1000 - i}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
data := make([]Person, len(people))
copy(data, people)
sort.Slice(data, func(i, j int) bool {
return data[i].Age < data[j].Age
})
}
}
Benchmarks typically show negligible difference. sort.Slice may have slight overhead from function calls, but modern Go compilers optimize this effectively.
Stability matters: sort.SliceStable guarantees that equal elements maintain their original order. Contrary to intuition, it has the same performance as sort.Slice in most cases, making it a better default choice.
Best Practices and Common Pitfalls
Choose sort.Slice for most scenarios—it’s clearer and more maintainable. Reserve sort.Interface for reusable sorting logic or when building public APIs.
// GOOD: Clear, concise, handles edge cases
sort.Slice(items, func(i, j int) bool {
return items[i].Priority > items[j].Priority
})
// PROBLEMATIC: Doesn't handle nil slices
var items []Item // nil slice
sort.Slice(items, func(i, j int) bool {
return items[i].Priority > items[j].Priority
}) // Works fine - sort.Slice handles nil slices gracefully
// GOOD: Use SliceStable when order matters for equal elements
sort.SliceStable(tasks, func(i, j int) bool {
return tasks[i].DueDate.Before(tasks[j].DueDate)
})
// PROBLEMATIC: Incorrect multi-field sorting
sort.Slice(employees, func(i, j int) bool {
// BUG: This always evaluates both conditions
return employees[i].Dept < employees[j].Dept &&
employees[i].Salary > employees[j].Salary
})
// GOOD: Correct multi-field sorting
sort.Slice(employees, func(i, j int) bool {
if employees[i].Dept != employees[j].Dept {
return employees[i].Dept < employees[j].Dept
}
return employees[i].Salary > employees[j].Salary
})
Always use pointer receivers when implementing sort.Interface for large structs to avoid copying. Test your comparison logic with equal values to ensure correct behavior. When in doubt, use sort.SliceStable—the stability guarantee prevents subtle bugs with no performance cost.
Custom sorting in Go is straightforward once you understand the patterns. Start with sort.Slice for simplicity, implement multi-field sorting with cascading comparisons, and reach for sort.Interface only when you need reusable sorting types.