Searching a slice often starts with a loop, and for unsorted data that can be the right choice. When the slice is already sorted, slices.BinarySearch gives you a more specific operation: it finds a target without scanning every element from the beginning, and it also tells you where a missing target belongs in the current order.

That second result is easy to overlook. It makes the function useful not only for membership checks, but also for maintaining sorted collections without writing separate insertion-point logic.

What slices.BinarySearch returns

The function accepts a slice of ordered values and a target:

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

It returns an index and a boolean. When the target exists, the boolean is true and the index points at the earliest matching element. When the target is absent, the boolean is false and the index is the position where that value would appear in sorted order.

package main

import (
    "fmt"
    "slices"
)

func main() {
    values := []int{2, 4, 4, 7, 9}

    index, found := slices.BinarySearch(values, 4)
    fmt.Println(index, found)

    index, found = slices.BinarySearch(values, 6)
    fmt.Println(index, found)
}

The output is:

1 true
3 false

For 4, index 1 is the first matching position. For 6, index 3 is the boundary between 4 and 7, which is exactly where 6 could be inserted.

Binary search requires sorted input

slices.BinarySearch expects the slice to be sorted in increasing order. It doesn’t sort the data for you, and it doesn’t report an error when that precondition is broken.

This call is valid:

values := []int{2, 4, 7, 9}
index, found := slices.BinarySearch(values, 7)

This one passes an invalid input shape:

values := []int{7, 2, 9, 4}
index, found := slices.BinarySearch(values, 7)

The second call still compiles, but its result isn’t a reliable search result because the ordering invariant is missing.

If your code receives a slice from an external caller and sorted order is part of the contract, validate it at that boundary with slices.IsSorted. If your code owns the data and simply needs an ordered slice, sorting once before repeated searches is often the cleaner design.

values := []int{9, 2, 7, 4}
slices.Sort(values)

index, found := slices.BinarySearch(values, 7)
fmt.Println(index, found) // 2 true

Sorting before every individual search usually defeats the point. Binary search is most useful when the ordering is established once and then reused across many lookups.

Duplicate values return the earliest match

A sorted slice can contain duplicates. Current slices.BinarySearch semantics return the earliest position at which the target is found.

values := []int{1, 3, 3, 3, 8}
index, found := slices.BinarySearch(values, 3)

fmt.Println(index, found) // 1 true

That behavior is handy when the first matching index is the boundary you need. It doesn’t directly give you the full duplicate range, though. If you need every matching element, the returned index is a good starting point for a short forward scan:

start, found := slices.BinarySearch(values, 3)
if found {
    end := start
    for end < len(values) && values[end] == 3 {
        end++
    }
    fmt.Println(values[start:end]) // [3 3 3]
}

For a collection with very large duplicate runs, a second boundary search can be preferable to scanning the whole run. For ordinary application data, the direct loop is often easier to read and maintain.

Use the insertion position for missing values

The index returned for a missing target is not a placeholder such as -1. It carries useful ordering information.

Suppose you maintain a sorted list of retry delays:

values := []int{5, 10, 30, 60}
index, found := slices.BinarySearch(values, 20)

fmt.Println(index, found) // 2 false

Index 2 says that 20 belongs before 30. You can combine that with slices.Insert:

if !found {
    values = slices.Insert(values, index, 20)
}

fmt.Println(values) // [5 10 20 30 60]

This keeps the ordering invariant intact for the next search.

There is a trade-off here. Finding the position is efficient, but inserting into the middle of a slice still requires shifting later elements. If your workload performs frequent arbitrary insertions into a large collection, a slice may not be the best data structure even though searching it is fast.

Empty slices and boundary positions

Empty input is a normal case. Searching an empty slice returns index 0 and false because the only possible insertion position is the start.

var values []int
index, found := slices.BinarySearch(values, 10)
fmt.Println(index, found) // 0 false

Targets smaller than every existing value also return 0:

values := []int{10, 20, 30}
index, found := slices.BinarySearch(values, 5)
fmt.Println(index, found) // 0 false

A target larger than every value returns len(values):

index, found = slices.BinarySearch(values, 40)
fmt.Println(index, found) // 3 false

That last case matters when you use the result as an index. len(values) is a valid insertion position, but it isn’t a valid element index. Check found before reading values[index].

index, found := slices.BinarySearch(values, 40)
if found {
    fmt.Println(values[index])
}

Reading values[index] unconditionally can panic when the target belongs after the final element.

Strings use their normal ordering

slices.BinarySearch works with the ordered types accepted by cmp.Ordered, including strings.

names := []string{"Ada", "Linus", "Rob"}
index, found := slices.BinarySearch(names, "Linus")
fmt.Println(index, found) // 1 true

The slice must use the same ordering that the search operation assumes. A list sorted with a case-insensitive rule is not necessarily sorted according to ordinary string comparison.

For application-specific ordering, struct fields, or a target type that differs from the element type, use slices.BinarySearchFunc instead. Its comparator defines the relationship between each slice element and the target.

A binary search needs sorted data. If you have a small unsorted slice and perform one lookup, a direct slices.Index or slices.Contains call can be simpler because it doesn’t require changing the data’s order first.

The balance shifts when the collection is already ordered or when many searches reuse the same sorted slice. In that situation, preserving the ordering gives each lookup a strong structural advantage without extra setup per query.

Also consider whether you need an index at all. If the real operation is keyed lookup by a stable identifier, a map may express the data model more directly. Binary search fits best when sorted sequence order is itself useful: ordered output, range operations, deterministic traversal, or compact read-heavy collections.

Keep the sorted invariant close to mutations

The easiest binary-search bug is usually not in the search call. It happens earlier, when code appends or modifies an element and silently breaks the ordering.

If a slice must stay searchable, make mutations preserve that property. For insertion, search for the position and insert there. For replacement, consider whether the new value still fits between its neighbors. For bulk updates, it may be simpler to modify the data and sort once afterward.

A small helper can make that contract explicit:

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

That helper also defines a duplicate policy: existing values are kept as-is rather than inserting another copy. If duplicates are valid, remove the found branch and insert at the returned position according to the behavior your application needs.

Treat the returned index as part of the API

slices.BinarySearch is more useful than a simple yes-or-no membership check. On a match, it gives the earliest matching position. On a miss, it gives the exact boundary where the target belongs.

Use it when sorted order is already a meaningful property of your data, keep that invariant intact as the slice changes, and always interpret the index together with found. That combination covers exact lookup, insertion points, empty input, and edge positions without custom binary-search code.