skip to content

What does sort.Slice give you over implementing sort.Interface, and what does it cost?

level: middleimportance: must knowfreq 68%

answer

  1. one closure instead of three methods
  2. the other two are filled in for you
  3. the first parameter is any
  4. reflect.Swapper, built once
  5. Slice is not stable, SliceStable is

basics

~20 s

sort.Slice takes the slice plus a less closure, so you skip declaring a named type with Len, Less and Swap. The cost: it accepts any, so a non-slice argument panics at run time instead of failing to compile, and it is not stable — sort.SliceStable is.

solid answer

~50 s

`sort.Slice(x any, less func(i, j int) bool)` sorts a slice given only an ordering closure, so a one-off ordering does not need a named type carrying three methods. It fills in the other two itself: `Len` from the slice's length and `Swap` from `reflect.Swapper(x)`, which is built once up front rather than per swap. The tradeoffs are the loss of compile-time typing — passing a map or an array panics at run time, not at build time — and that the closure receives *indices*, so it must index the slice it closes over rather than capture element values. `sort.Slice` is not stable; `sort.SliceStable` keeps the input order of elements the closure treats as equal. When the same ordering is used in several places, a named `sort.Interface` type still reads better and gives that ordering a name.

code

go · 6 lines
go
sort.Slice(rows, func(i, j int) bool {
	if rows[i].Score != rows[j].Score {
		return rows[i].Score > rows[j].Score
	}
	return rows[i].ID < rows[j].ID
})

go deeper

for a junior

Know the shape: sort.Slice takes the slice and a func(i, j int) bool, and the closure indexes the slice directly. Remember sort.SliceStable exists for when equal elements must keep their input order.

for a middle

Explain what the package fills in for you — length by reflection, swapping via reflect.Swapper built once — and the two costs: an any parameter that panics at run time, and a closure that sees moving indices.

for a senior

Show judgment about when a named ordering type earns its keep: reuse, testability, wrapping, and non-slice containers. Be able to diagnose the parallel-key-slice bug from a report whose rows come out mis-ranked.

for a principal

Treat the choice as a codebase convention rather than a per-call taste: closures scattered across services let the same ranking drift apart, while a shared exported ordering type becomes something other teams depend on and you cannot change quietly.

## What it is ```go func Slice(x any, less func(i, j int) bool) func SliceStable(x any, less func(i, j int) bool) func SliceIsSorted(x any, less func(i, j int) bool) bool ``` All three take the slice itself plus one closure. Compared with implementing `sort.Interface`, you supply only the ordering: ```go sort.Slice(rows, func(i, j int) bool { if rows[i].Score != rows[j].Score { return rows[i].Score > rows[j].Score } return rows[i].ID < rows[j].ID }) ``` No named type, no `Len`, no `Swap`. For an ordering used once, in one function, this is the shorter and clearer form, and it is why most everyday Go sorts are written this way. ## How it fills in the missing methods `Len` is trivial — the package takes the slice's length by reflection once. `Swap` is the interesting one: `sort.Slice` calls `reflect.Swapper(x)`, which returns a `func(i, j int)` specialised to that slice's element type. The reflection work happens **once**, when the sort starts; the returned function then swaps by moving bytes, so the per-swap cost is an indirect call rather than a reflective operation. The same is true of `less`: it is an ordinary function value called indirectly. In practice a `sort.Slice` call and an equivalent `sort.Interface` implementation are in the same performance neighbourhood; neither inlines the comparison, and if a benchmark says the ordering is your bottleneck, the fix is usually a cheaper key rather than a different API. ## The costs **Run-time typing.** The first parameter is `any`. Pass something that is not a slice — a map, an array, a pointer to a slice, a `nil` interface — and the call **panics at run time**. The compiler cannot help you. This is the price of a pre-generics API that must accept every slice type. **The closure gets indices, not elements.** `less(i, j int)` receives positions in the slice *as it currently is*, mid-sort, after arbitrary swaps. The closure must therefore index the slice it closes over. Capturing element values beforehand is a real bug: ```go // WRONG: keys never move, but the elements do keys := make([]int, len(rows)) for i, r := range rows { keys[i] = r.Score } sort.Slice(rows, func(i, j int) bool { return keys[i] < keys[j] }) ``` After the first swap, `keys[i]` no longer describes `rows[i]`, and the result is nonsense. Precomputed keys have to live *inside* the elements being swapped — add a field to the struct, or sort a slice of `{key, row}` pairs — so that swapping moves the key with its row. **No name for the ordering.** A closure cannot be reused, documented, or tested on its own the way a `type byRank []Row` with a `Less` method can. When the same ranking is applied in three places, three closures drift apart; a named type gives the ordering an identity, and a `Less` method can be unit-tested pair by pair. ## Stability `sort.Slice` makes **no guarantee** about elements the closure reports as equal — they may end up in any relative order. `sort.SliceStable` keeps them in their input order. The same split exists for the interface-based API: `sort.Sort` versus `sort.Stable`. Choosing the stable variant is a decision about the *output*, not about performance folklore: if your ordering has a unique final tie-breaker, no elements are ever equal and the two behave identically. ## Which to reach for A reasonable default: use `sort.Slice` for a local, one-off ordering inside a single function, and implement `sort.Interface` when the ordering is part of a package's vocabulary, when it must be reused or reversed by name, when it must be unit-tested independently, or when the thing being sorted is not a plain slice at all — parallel slices, a struct wrapping a slice, or any container that can only expose count, compare and swap.

  • What happens if the first argument to sort.Slice is not a slice?
    It panics at run time. The parameter's type is `any`, so the check happens through reflection when the call executes, not at compile time. A map, an array, a pointer to a slice or a nil interface all get past the compiler and blow up in production, which is the main safety difference from implementing `sort.Interface` on a concrete type.
  • Why can't the less closure capture precomputed keys in a parallel slice?
    Because the closure is handed current indices while elements are being swapped underneath it. The parallel slice is never swapped, so after the first exchange `keys[i]` describes a different row than `rows[i]`. Precomputed keys must travel with the element — a field on the struct, or a slice of key-plus-row pairs — so a swap moves both.
  • Does sort.Slice's use of reflection make it much slower than a sort.Interface implementation?
    Not meaningfully. `reflect.Swapper` is called once to build a type-specialised swap function, so no reflection happens per swap; from then on both APIs pay an indirect call per comparison and per move. If sorting really is hot, the win comes from a cheaper comparison key or from sorting less data, not from switching between the two APIs.
  • When would you still write a named sort.Interface type?
    When the ordering deserves a name: it is reused across functions or packages, it needs its own unit tests, it will be wrapped by `sort.Reverse`, or the data is not a plain slice — parallel slices, a struct that owns a slice, or any container that can only expose count, compare and swap. A closure buried in one function cannot be any of those.

saying these in an interview costs you the question

  • Thinks sort.Slice is checked at compile time
  • Says sort.Slice is stable, or that Go's sort is stable by default
  • Believes the closure receives element values rather than indices
  • Keeps sort keys in a parallel slice that is never swapped
  • Claims reflection is used on every swap so sort.Slice is far slower