Searching a slice is easy when the element itself is comparable and you already know the exact value. Real programs often need something slightly different: the first queued job, the first expired token, or the first record whose normalized name matches some input.

slices.IndexFunc handles that case directly. You provide a predicate, and it returns the index of the first element for which that predicate is true. If nothing matches, it returns -1.

Find the first matching element with slices.IndexFunc

Suppose a queue is represented as a slice of structs and you need the first job waiting to run:

package main

import (
    "fmt"
    "slices"
)

type Job struct {
    ID    int
    State string
}

func main() {
    jobs := []Job{
        {ID: 1, State: "done"},
        {ID: 2, State: "queued"},
        {ID: 3, State: "queued"},
    }

    i := slices.IndexFunc(jobs, func(job Job) bool {
        return job.State == "queued"
    })

    fmt.Println(i)          // 1
    fmt.Println(jobs[i].ID) // 2
}

The predicate is evaluated from the beginning of the slice. The search stops at the first match, so the second queued job is never needed to determine the result.

That first-match behavior matters when order has meaning. If the slice is ordered by arrival time, priority, or some earlier processing step, IndexFunc preserves that ordering decision rather than finding an arbitrary matching element.

Check for -1 before indexing the slice

The most common mistake is treating the result as a valid index without checking it first. IndexFunc returns -1 when no element satisfies the predicate, and using slice[-1] panics.

Handle the missing case before reading the element:

i := slices.IndexFunc(jobs, func(job Job) bool {
    return job.State == "failed"
})

if i == -1 {
    fmt.Println("no failed job")
    return
}

fmt.Println(jobs[i])

An empty slice follows the same rule. The predicate has no elements to inspect, so the result is -1. A nil slice also returns -1; there is no special nil case you need to add around the call.

Use IndexFunc when equality is not enough

slices.Index is the simpler choice when you are searching for an exact comparable value. IndexFunc earns its place when matching requires logic.

For example, a case-insensitive string lookup can put the normalization rule in the predicate:

services := []string{"API", "worker", "scheduler"}

i := slices.IndexFunc(services, func(service string) bool {
    return strings.EqualFold(service, "api")
})

fmt.Println(i) // 0

The same pattern works for ranges, timestamps, nested fields, or combinations of conditions:

i := slices.IndexFunc(jobs, func(job Job) bool {
    return job.State == "queued" && job.ID > 10
})

Keep the predicate focused on the matching rule. If it also mutates unrelated state, logs heavily, or performs expensive I/O, a compact search expression can become surprisingly hard to reason about.

IndexFunc returns an index, not the element

Sometimes the index is exactly what you need. You may want to replace the matching element, inspect its neighbors, remove it, or retain its position for later logic.

i := slices.IndexFunc(jobs, func(job Job) bool {
    return job.ID == 42
})
if i >= 0 {
    jobs[i].State = "running"
}

If you only need to know whether any element matches, slices.ContainsFunc communicates that intent more directly. It returns a boolean instead of making callers interpret -1.

Likewise, if you need every matching element, repeatedly calling IndexFunc on progressively smaller subslices is usually less clear than a single loop that collects the matches. IndexFunc is designed around one question: where is the first match?

The search is linear

slices.IndexFunc examines elements in order until it finds a match or reaches the end. That makes it a natural fit for ordinary slice searches, especially when the slice is small or when searches are infrequent.

For a large collection queried repeatedly by the same key, scanning from the beginning each time may be the wrong data structure. A map can provide direct lookup when a stable key exists. If the data is sorted and the predicate can be expressed as an ordering comparison, binary search may also fit better.

The predicate’s own cost matters too. A cheap field comparison keeps the scan cheap per element; an expensive predicate multiplies that work across every element inspected before the match.

Prefer the operation that matches the question

Use slices.IndexFunc when you need the position of the first element satisfying a condition. Check for -1 before indexing, and keep in mind that the search follows slice order and stops at the first match.

If the requirement changes, choose the narrower operation instead: slices.Index for exact values, slices.ContainsFunc for a yes-or-no answer, a map for repeated keyed lookup, or an explicit loop when you need multiple matches or more control over the traversal. That keeps the code aligned with what the search is actually trying to answer.