Theory

The rule that reconciles all three

Three measurements, three different answers, one rule.

lesson standard library result
3 container/list a tie — 50 hops against 50
6 sort.Search won — 15 ns against 18
13 container/heap lost — by 1.66×

The temptation is to draw a conclusion about the standard library. But three different answers mean the question was never about it.

The rule

An abstraction is free when the compiler can see through it. It is expensive when it forces THE DATA TO BE BOXED on an operation too small to hide the cost.

(Drill 4 separates boxing from table dispatch and shows that boxing is what decides it here. Dispatch costs something too — just not a measurable amount in this case.)

Two conditions, and the second matters as much as the first. A call through a table costs about the same whatever it wraps — one comparison or an entire file read. What changes is not the cost but the ratio.

How the three fit

Lesson 6 — the compiler sees everything. sort.Search is handed a closure created right there. The concrete type is known, so Go inlines it; after optimisation what remains is the same loop you wrote by hand. Zero boxing, zero calls. The abstraction disappears.

Lesson 13 — the compiler sees nothing. container/heap demands an interface. Elements travel through any (boxing, 20,000 allocations), and Less and Swap go through a table. And the operation being wrapped is one comparison — the smallest there is. The ratio is as bad as it gets.

Lesson 3 — there was nothing to see through. container/list's methods are defined on concrete types (*List, *Element), not on an interface. No table, no dynamic dispatch. The only any in it is the stored Value, boxed once when an element is inserted — and lesson 3 was measuring the walk, not the insert. Hence the tie: nothing was in the way.

What follows in practice

Not "use the standard library" and not "hand-roll hot code". Neither survives three measurements in a row.

Ask three questions:

  1. Is the type known at the call site? A closure, a concrete type, a generic parameter — yes. An interface — no.
  2. Does the data cross an any boundary? If so, anything bigger than a pointer is copied to the heap. An Item is 56 bytes; an interface value is 16.
  3. How big is the operation being wrapped? One comparison — the cost shows. A file read — you will never notice it.

"Yes, no, small" is sort.Search. "No, yes, small" is container/heap. Which is exactly how they came out.

And when any of it matters

In lesson 6 sort.Search was faster and shorter and already tested. That choice is easy.

Here the hand-written heap wins by 1.66× — but that is 0.8 ms over 10,000 elements. Lesson 6 already showed what differences of that size look like beside real work: transit's trie argument fits entirely inside a tenth of a millisecond and decides nothing because of it.

1.66× matters when the operation runs millions of times. Your algo top does not. Dijkstra on a 10,000-node graph does, and step 8 shows what it costs in practice.

So the answer is not "hand-roll your heaps". The answer is: know why one abstraction is free and another is not, and be able to measure which case you are in.