When a Go slice is already sorted, scanning it from the beginning to find one value throws away useful information. slices.BinarySearch uses that ordering directly. It returns both an index and a found flag, and the index remains useful even when the target isn’t present.

That second behavior is easy to overlook. slices.BinarySearch isn’t only a membership check; it also tells you where a missing value belongs if you want to preserve the slice’s sort order.

What slices.BinarySearch returns

For ordered element types, the function has this shape:

func BinarySearch[S ~[]E, E cmp.Ordered](x S, target E) (int, bool)

The input slice must be sorted in increasing order. If the target exists, found is true and the returned index points to the earliest matching element. If it doesn’t exist, found is false and the index is the position where the target would appear in the sort order.

package main

import (
    "fmt"
    "slices"
)

func main() {
    numbers := []int{10, 20, 20, 20, 40}

    index, found := slices.BinarySearch(numbers, 20)
    fmt.Println(index, found)
}

The output is:

1 true

There are three copies of 20, but the returned index is 1, the first position containing that value. This makes the result predictable when duplicates are present.

A missing value still gives you useful information

Consider searching the same slice for 30:

numbers := []int{10, 20, 20, 20, 40}

index, found := slices.BinarySearch(numbers, 30)
fmt.Println(index, found)

The result is:

4 false

Index 4 currently contains 40. That’s exactly where 30 would need to be inserted so the slice stays sorted.

The boundary cases follow the same rule. A target smaller than every element returns index 0; a target larger than every element returns len(numbers).

low, lowFound := slices.BinarySearch(numbers, 5)
high, highFound := slices.BinarySearch(numbers, 50)

fmt.Println(low, lowFound)   // 0 false
fmt.Println(high, highFound) // 5 false

Notice that a valid insertion point can equal the slice length. Code that uses the returned index must not index into the slice before checking found or checking the boundary.

This is unsafe:

index, _ := slices.BinarySearch(numbers, 50)
fmt.Println(numbers[index]) // index == len(numbers)

If your goal is to inspect an existing match, check found first.

Insert a missing value while keeping the slice sorted

The insertion-point result pairs naturally with slices.Insert.

package main

import (
    "fmt"
    "slices"
)

func insertSorted(numbers []int, value int) []int {
    index, found := slices.BinarySearch(numbers, value)
    if found {
        return numbers
    }

    return slices.Insert(numbers, index, value)
}

func main() {
    numbers := []int{10, 20, 40}
    numbers = insertSorted(numbers, 30)
    fmt.Println(numbers)
}

This prints:

[10 20 30 40]

The helper above deliberately avoids inserting duplicates. If duplicates are allowed, you can ignore found and insert at the returned position, but remember what that means: a new matching value will be inserted before existing equal values because BinarySearch returns the earliest matching position.

For occasional insertions into a modest sorted slice, this can be pleasantly simple. For heavy insertion workloads, a slice may be the wrong data structure. Finding the position is only part of the cost; inserting into the middle can require shifting the elements that follow it.

The sorted-input requirement is part of the contract

The most common mistake with slices.BinarySearch is treating it like a faster slices.Index on arbitrary data.

numbers := []int{40, 10, 30, 20}
index, found := slices.BinarySearch(numbers, 30)

That input doesn’t satisfy the function’s requirement because it isn’t sorted in increasing order. Don’t rely on whatever result happens to come back.

If the slice is unsorted and you only need one lookup, a linear search is usually the straightforward choice. Sorting first changes the slice and costs work of its own. Binary search becomes more attractive when the data is already sorted for another reason or when you can sort once and perform many lookups afterward.

If mutating the original order isn’t acceptable, clone before sorting:

ordered := slices.Clone(numbers)
slices.Sort(ordered)

index, found := slices.BinarySearch(ordered, 30)

Be aware that the returned index now refers to ordered, not to the original slice. If you need the original position, sorting a copy doesn’t preserve that mapping by itself.

Empty and nil slices don’t need special handling

Searching an empty slice returns the only possible insertion point: zero.

var numbers []int

index, found := slices.BinarySearch(numbers, 7)
fmt.Println(index, found)

The result is:

0 false

The same result applies to an empty non-nil slice. There is no need to add a separate len(numbers) == 0 branch just to call BinarySearch safely.

Use BinarySearchFunc when ordering isn’t built in

slices.BinarySearch is designed for values covered by Go’s ordered constraint, such as strings and numeric types. Real programs often search slices of structs, or they sort strings according to domain-specific rules.

For those cases, slices.BinarySearchFunc accepts a comparison function. The critical detail is that the comparison used for searching must describe the same ordering used to sort the slice. If you sort records by one field and search them as though they were ordered by another, the binary-search precondition is broken even if the slice looks orderly to a human reader.

For example, if a User slice is sorted by numeric ID, search it with a comparator that compares IDs. If it is sorted by normalized username, the search comparator must use that same normalization and ordering rule.

That consistency matters more than the choice between the two APIs. Binary search can only discard half of the remaining search space when the ordering tells the truth.

Binary search is most useful when sorted order already pays for itself

slices.BinarySearch is a good fit when a slice is already maintained in sorted order, or when one sorting pass supports many later searches. It gives you a direct membership result, handles duplicate matches predictably, and turns a miss into a useful insertion point.

Before using the returned index, decide which meaning you need. If found is true, it identifies the earliest match. If found is false, it identifies a boundary between smaller and larger values and may equal len(slice). Keeping those two cases explicit makes the code both safer and easier to read.