Sorting a Go slice by date: from sort.Interface to go-timesort
You have a slice of structs with a time.Time field. You want them sorted chronologically. There are three ways to do this in Go today — and they’re not interchangeable.
sort.Interface: the classic approach
The canonical pre-Go 1.21 approach: implement sort.Interface on a wrapper type.
type Event struct {
Name string
CreatedAt time.Time
}
type ByDate []Event
func (a ByDate) Len() int { return len(a) }
func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByDate) Less(i, j int) bool { return a[i].CreatedAt.Before(a[j].CreatedAt) }
// Usage
sort.Sort(ByDate(events))
It works. It’s readable. The problem: you have to repeat this pattern for every struct you want to sort by date — ByDateUser, ByDateOrder, ByDateInvoice… the boilerplate compounds quickly.
Go 1.21: slices.SortFunc, the modern approach
Since Go 1.21, the standard library’s slices package provides slices.SortFunc. Its comparison function expects an integer return — negative, zero, or positive — which is exactly what time.Time.Compare() returns.
import "slices"
slices.SortFunc(events, func(a, b Event) int {
return a.CreatedAt.Compare(b.CreatedAt)
})
That’s it. No wrapper type, no methods to implement. For descending order, flip it:
slices.SortFunc(events, func(a, b Event) int {
return b.CreatedAt.Compare(a.CreatedAt)
})
time.Time.Compare() was added in Go 1.20. Before that, you could use a.CreatedAt.Before(b.CreatedAt) with a ternary, but Compare() integrates directly with slices.SortFunc without any gymnastics.
For the vast majority of use cases, slices.SortFunc is the right answer. No external dependency, no boilerplate.
When that’s not enough
slices.SortFunc handles simple cases well — one sort criterion, one struct, one-off usage. The friction shows up when you find yourself writing the same pattern repeatedly:
- multiple different structs, each with their own date field
- stable sort required (preserve relative order of equal-date elements)
- repeated operations on the same slice (asc in one context, desc in another)
- need to clone the slice before sorting without mutating the original
At that point, you either wrap slices.SortFunc in hand-rolled helpers, or you reach for a generic abstraction.
go-timesort: the generic API
go-timesort is a lightweight library I wrote to encapsulate exactly this pattern. It uses Go generics (1.18+) and has zero external dependencies.
go get github.com/azrod/go-timesort
The core idea: you create a sorter by passing your slice and a function that extracts the time.Time from each element.
import gts "github.com/azrod/go-timesort"
type User struct {
Name string
CreatedAt time.Time
}
users := []User{
{Name: "Alice", CreatedAt: time.Date(2022, 5, 1, 0, 0, 0, 0, time.UTC)},
{Name: "Bob", CreatedAt: time.Date(2021, 8, 15, 0, 0, 0, 0, time.UTC)},
{Name: "Carol", CreatedAt: time.Date(2023, 1, 20, 0, 0, 0, 0, time.UTC)},
}
sorter := gts.New(users, func(u User) time.Time {
return u.CreatedAt
})
sorter.SortAsc()
fmt.Println(sorter.Items()) // Bob, Alice, Carol
sorter.SortDesc()
fmt.Println(sorter.Items()) // Carol, Alice, Bob
Stable sort and cloning are available via SortStableAsc(), SortStableDesc(), and Clone():
// Stable sort: preserves relative order of elements with identical dates
sorter.SortStableAsc()
// Clone: returns a new sorted slice without mutating the original
sorted := sorter.Clone().SortAsc().Items()
Stable sort matters when your slice can contain elements with identical timestamps and their relative order has business significance — audit logs, transactional events, and similar ordered sequences.
Which tool for which case
| Case | Recommended approach |
|---|---|
| One-off sort, one struct, one criterion | slices.SortFunc |
| Multiple needs (asc/desc, stable, clone) | go-timesort |
| Multiple different structs to sort by date | go-timesort |
| Go < 1.21 | sort.Interface or go-timesort |
sort.Interface is still relevant on a pre-1.21 codebase, or when your comparison logic goes beyond a single date field. For everything else: slices.SortFunc — go-timesort once the pattern starts repeating.