Sorting a Go slice by date: from sort.Interface to go-timesort

Article
🇫🇷 This article is also available in Français

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)
})
Note

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()
Conseil

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

CaseRecommended approach
One-off sort, one struct, one criterionslices.SortFunc
Multiple needs (asc/desc, stable, clone)go-timesort
Multiple different structs to sort by datego-timesort
Go < 1.21sort.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.SortFuncgo-timesort once the pattern starts repeating.

← Back to articles