Code

The boundaries — this is where it breaks

Create search_test.go (see the panel). We write this test; your job is to make LowerBound pass it.

The happy path — "the item is somewhere in the middle" — is easy. Every case below is where a hand-rolled binary search actually breaks.

Ten boundary cases

$ go test -run TestLowerBoundBoundaries -v .
=== RUN   TestLowerBoundBoundaries/exact_match,_first
=== RUN   TestLowerBoundBoundaries/exact_match,_middle
=== RUN   TestLowerBoundBoundaries/exact_match,_last
=== RUN   TestLowerBoundBoundaries/between_two_items
=== RUN   TestLowerBoundBoundaries/before_everything
=== RUN   TestLowerBoundBoundaries/AFTER_everything
=== RUN   TestLowerBoundBoundaries/empty_slice
=== RUN   TestLowerBoundBoundaries/single_item,_before
=== RUN   TestLowerBoundBoundaries/single_item,_exact
=== RUN   TestLowerBoundBoundaries/single_item,_after
--- PASS: TestLowerBoundBoundaries (0.00s)

Three of them deserve their own paragraph.

"AFTER everything" returns 4 for a 4-item library. An index that does not exist. That is exactly why hi starts at len(items) and not len(items)-1: the answer "past the end" needs somewhere to live. Start at len-1 and this case silently returns the last item instead.

An empty slice returns 0 and never enters the loop, because lo < hi is false immediately. The zero case takes care of itself — if the invariant is right.

A one-item library is checked three times, because mid is always 0 there and a mistake with mid + 1 turns into an infinite loop.

Duplicates: which of the three?

lib := sorted("a", "b", "b", "b", "c")
LowerBound(lib, "b")  // → 1

Three "b"s. The correct answer is the first, index 1. That is what lower bound means.

If your search returns 2 or 3 it looks right and will survive a casual check. But code that scans forward from the result to collect every match will miss some of them — and the bug will surface somewhere else entirely.

transit tests its search semantics rather than assuming them, for the same reason.

And the most important test

$ go test -run TestUnsortedInputSilentlyLies -v .
    search_test.go:93: unsorted input: linear says 0, binary says 2 —
                       binary is wrong, and silent

Unsorted input is not an error to a binary search. It reports nothing. It simply returns the wrong number.

Gotcha

Sorted order is a precondition, not a recommendation. Linear search always works; binary search works only when its condition holds, and it does not check — checking would cost O(n) and destroy the entire point. Preconditions are your program's job to protect, not the search's.