What does sort.Slice give you over implementing sort.Interface, and what does it cost?
answer
- one closure instead of three methods
- the other two are filled in for you
- the first parameter is any
- reflect.Swapper, built once
- Slice is not stable, SliceStable is
basics
~20 ssort.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 linessort.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
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.
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.
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.
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