slices.IndexFunc scans a slice from the beginning and returns the index of the first element accepted by a predicate. That contract is narrower than filtering: only one position is requested, and the scan has no reason to continue after a match.
Its standard-library signature accepts any slice element type:
func IndexFunc[S ~[]E, E any](s S, f func(E) bool) intThe predicate receives each element in index order. A true result ends the search and produces that index. If every call returns false, the function returns -1.
The first match defines the result
Consider a slice containing several values that satisfy the same condition:
package main
import (
"fmt"
"slices"
)
func main() {
values := []int{12, 7, -3, 9, -8}
index := slices.IndexFunc(values, func(v int) bool {
return v < 0
})
fmt.Println(index) // 2
}Both -3 and -8 satisfy the predicate, but the result is 2. The later match is not relevant once the earlier one has been found.
This makes slice order part of the operation’s meaning. Reordering the same values can change the returned index even when the set of matching values remains unchanged.
A missing match uses a sentinel index
IndexFunc returns -1 when no element satisfies the predicate:
values := []int{2, 4, 8}
index := slices.IndexFunc(values, func(v int) bool {
return v%2 != 0
})
fmt.Println(index) // -1The sentinel is outside the valid slice index range, so callers can branch on index >= 0 before indexing the slice.
An empty or nil slice also produces -1 because there are no elements to test. In those cases the predicate is not called.
Predicate search supports non-comparable elements
Plain slices.Index searches for a value using equality and therefore requires a comparable element type. IndexFunc instead delegates the match decision to a function and places no comparable constraint on E.
That difference permits searches over slices whose elements contain slices, maps, or other non-comparable fields:
type Batch struct {
ID string
Samples []int
}
batches := []Batch{
{ID: "alpha", Samples: []int{2, 4}},
{ID: "beta", Samples: []int{3, 5}},
}
index := slices.IndexFunc(batches, func(b Batch) bool {
return len(b.Samples) == 2 && b.Samples[0]%2 != 0
})
fmt.Println(index) // 1The function does not need to define equality for the complete Batch value. It only states the condition that identifies a match.
Predicate side effects affect observable behavior
Because evaluation stops at the first successful predicate call, a predicate with side effects may run fewer times than the slice length.
calls := 0
values := []int{4, 6, 7, 9}
index := slices.IndexFunc(values, func(v int) bool {
calls++
return v%2 != 0
})
fmt.Println(index) // 2
fmt.Println(calls) // 3Relying on those side effects can make search code harder to reason about. A predicate that only evaluates its input keeps the stopping behavior local to the search result.
The same early stop can matter when predicate evaluation itself has meaningful cost. Work associated with elements after the first match is not performed by IndexFunc.
IndexFunc returns a position, not an element
The integer result is useful when surrounding code needs the location for a later slice operation. The caller can inspect, replace, or remove the matched element using the returned position.
index := slices.IndexFunc(values, func(v int) bool {
return v < 0
})
if index >= 0 {
values[index] = 0
}When code needs only a boolean existence check, slices.ContainsFunc expresses that narrower result directly. Both operations stop after the first accepted element, but one exposes its position while the other reports only presence.
slices.IndexFunc fits cases where a predicate defines membership and the position of the earliest match carries value. Its linear scan, first-match rule, and -1 sentinel make the boundary explicit without requiring a separate filtered slice or a custom search loop.