📖 Analogy
Guessing a number between 1 and 100: a friend says “higher” or “lower” after each guess. Start at 50, then 75 or 25, then halve again — you corner the answer in about seven guesses, not a hundred. Binary search is exactly that, run against a sorted slice: every comparison throws away half of what’s left.
Halve the range each step
Binary search keeps a window [lo, hi] of indices that could still hold the target. Look at the middle; if it’s too small, the answer must be to the right, so move lo past mid; if it’s too big, move hi before mid. Each step discards half the window, so n elements take about log₂(n) steps — 20 for a million.
graph TD A["lo=0 hi=15 mid=7<br/>a[7] < target → go right"] --> B["lo=8 hi=15 mid=11<br/>a[11] > target → go left"] B --> C["lo=8 hi=10 mid=9<br/>a[9] == target → found"]
The whole algorithm is one loop. The details that trip people up are all about boundaries: the loop condition and how you move lo and hi.
package main
import "fmt"
// binarySearch returns the index of target in the sorted slice a,
// or -1 if it is not present. Runs in O(log n).
func binarySearch(a []int, target int) int {
lo, hi := 0, len(a)-1 // inclusive window [lo, hi]
for lo <= hi { // note: <=, so a one-element window is still checked
mid := lo + (hi-lo)/2 // avoids lo+hi overflow on huge slices
switch {
case a[mid] == target:
return mid
case a[mid] < target:
lo = mid + 1 // target is to the right; drop mid and left half
default:
hi = mid - 1 // target is to the left; drop mid and right half
}
}
return -1
}
func main() {
a := []int{1, 3, 4, 7, 9, 11, 15, 22, 30, 41}
for _, target := range []int{7, 22, 8} {
idx := binarySearch(a, target)
fmt.Printf("search %2d -> index %d\n", target, idx)
}
}
The off-by-one traps
Three choices have to agree, or the loop spins forever or skips a value:
- Loop condition
lo <= hi. With an inclusive window,lo == hiis a real one-element range you must still inspect. Using<instead would miss it. lo = mid + 1andhi = mid - 1. You already comparedmid, so exclude it. Writinglo = mid(without+1) whena[mid] < targetleaves the window the same size and loops forever.mid := lo + (hi-lo)/2. Integer division rounds down, which keepsmidinside[lo, hi)when the window has two or more elements — exactly what the+1/-1updates rely on.
Reach for the standard library
You rarely hand-roll this in production. The stdlib has two ready-made tools:
slices.BinarySearch(Go 1.21+) — on an ordered slice it returns(index, found): the position if present, otherwise the index where the value would be inserted.sort.Search— the general form. You give itnand a predicatef(i)that isfalse … false true … true; it returns the first index wherefistrue. That’s the insertion point, so you check it yourself to confirm a real match.
This second form is the key idea: binary search isn’t only about slices. Any time you have a monotonic predicate — something that flips from false to true exactly once as a parameter grows — you can binary-search the smallest input that satisfies it. That’s “binary search on the answer”: instead of an index, you halve a numeric range (a speed, a capacity, a time) and probe the predicate at the midpoint.
package main
import (
"fmt"
"slices"
"sort"
)
func main() {
a := []int{1, 3, 4, 7, 9, 11, 15, 22, 30, 41}
target := 22
// slices.BinarySearch returns (index, found).
idx, found := slices.BinarySearch(a, target)
fmt.Printf("slices.BinarySearch: index=%d found=%t\n", idx, found)
// sort.Search returns the first index where the predicate is true,
// i.e. the first element >= target. Confirm the match yourself.
j := sort.Search(len(a), func(i int) bool { return a[i] >= target })
hit := j < len(a) && a[j] == target
fmt.Printf("sort.Search: index=%d found=%t\n", j, hit)
// Both stdlib helpers agree with each other.
fmt.Printf("agree: %t\n", idx == j && found == hit)
}
⚠️ Sorted, and the right order
Binary search silently returns wrong answers on unsorted data — there’s no error, just a miss. And the order must match the comparison: slices.BinarySearch and sort.Search with >= assume ascending order. If your slice is sorted descending, or by a custom key, you must use slices.BinarySearchFunc / sort.Search with a predicate that matches that order. Sorting once is O(n log n); if you search many times, that cost pays for itself.
See also
- sorting — the prerequisite; binary search needs ordered data.
- Big-O — O(log n), the canonical logarithmic algorithm.
- two pointers — the other sorted-data sweep.
- binary search trees — the same halving logic as a structure.
Next: how the slice gets sorted in the first place — sorting.
Related topics
Measure cost before you optimize — Big-O for time and space, the common growth classes, and how to read the complexity of real Go code.
algorithmsSortingWhy comparison sorts can't beat O(n log n) — merge sort and quicksort by hand, then the one line you actually write in Go: slices.Sort.
linearArrays & SlicesA Go slice is a view over a backing array — how the (ptr, len, cap) header, append, growth, copy and re-slicing actually behave, and the aliasing traps.
Check your understanding
Score: 0 / 51. What must be true about the slice before you binary-search it?
Binary search decides which half to keep by comparing against the middle element. That decision is only valid if the data is ordered; on an unsorted slice it discards the wrong half and misses the target.
2. Why write `mid := lo + (hi-lo)/2` instead of `mid := (lo+hi)/2`?
For huge slices `lo+hi` can exceed the integer range and wrap negative. `hi-lo` is always within range, so `lo + (hi-lo)/2` is the safe, equivalent form.
3. What does `sort.Search` return when the target is not present?
`sort.Search` finds the smallest index where a predicate becomes true — the insertion point. You then check that index yourself to confirm whether the value actually matched.
4. What is 'binary search on the answer'?
Whenever a yes/no test flips from false to true exactly once as a parameter grows (e.g. 'can we finish in ≤ t time?'), you can binary-search the smallest value that passes — probing the predicate at each midpoint. It's the same halving logic applied to an answer space, not an array.
5. To find the FIRST element ≥ x (lower bound) among possible duplicates, what do you use?
A textbook binary search returns *some* matching index, not necessarily the first. A predicate-based search (a[i] >= x) returns the leftmost position the condition becomes true — the lower bound — which is what you want for counting duplicates or finding ranges. Use > x for the upper bound.
Comments
Sign in with GitHub to join the discussion.