Sometimes you don’t need the matching element or its position. You only need to answer a yes-or-no question: does this slice contain anything that satisfies a condition?

For that case, slices.ContainsFunc is more direct than writing an index loop or calling slices.IndexFunc and comparing its result with -1. It accepts a predicate, checks elements in order, and returns as soon as one matches.

What slices.ContainsFunc does

The function accepts any slice element type because the predicate decides what counts as a match:

func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool

It returns true when at least one element makes f return true. If no element matches, it returns false.

That makes it useful for slices of structs, where direct value lookup often isn’t the question you want to ask. Suppose a worker needs to know whether any enabled job has already been retried:

package main

import (
    "fmt"
    "slices"
)

type Job struct {
    Name    string
    Retries int
    Enabled bool
}

func main() {
    jobs := []Job{
        {Name: "sync", Retries: 0, Enabled: true},
        {Name: "backup", Retries: 2, Enabled: false},
        {Name: "email", Retries: 1, Enabled: true},
    }

    hasRetriedEnabledJob := slices.ContainsFunc(jobs, func(job Job) bool {
        return job.Enabled && job.Retries > 0
    })

    fmt.Println(hasRetriedEnabledJob) // true
}

The predicate states the condition in the same place as the search. There’s no sentinel index to interpret afterward and no temporary filtered slice to allocate.

Use ContainsFunc when the answer is boolean

ContainsFunc overlaps with several other slice helpers, but the return value is a useful way to choose between them.

If you need to know whether the exact comparable value "ready" occurs in a []string, use slices.Contains. If you need the index of the first matching struct, use slices.IndexFunc. When the caller only needs existence according to a custom rule, ContainsFunc matches that requirement directly.

For example, checking a prefix doesn’t require an index:

names := []string{"api", "backup", "worker"}

hasBackupJob := slices.ContainsFunc(names, func(name string) bool {
    return strings.HasPrefix(name, "back")
})

A handwritten version is straightforward too:

hasBackupJob := false
for _, name := range names {
    if strings.HasPrefix(name, "back") {
        hasBackupJob = true
        break
    }
}

ContainsFunc isn’t doing something a loop can’t do. Its advantage is that the operation is recognizable at a glance: this code is testing whether any element satisfies a predicate.

The search stops at the first match

ContainsFunc short-circuits. Once the predicate returns true, later elements aren’t inspected.

You can see that by counting calls:

jobs := []string{"api", "backup", "worker"}
calls := 0

found := slices.ContainsFunc(jobs, func(name string) bool {
    calls++
    return strings.HasPrefix(name, "ba")
})

fmt.Println(found) // true
fmt.Println(calls) // 2

The worker element is never passed to the predicate.

This is useful when matches tend to appear early or when the predicate does nontrivial work. It also means the callback is the wrong place for side effects that must run for every element. Don’t use it to update every record, accumulate complete statistics, or perform validation that must inspect the whole slice. A successful match can end the traversal immediately.

Keep predicates focused on the condition

Predicates are easiest to reason about when they answer one question without changing surrounding state.

Consider an access check:

type Membership struct {
    UserID string
    Role   string
    Active bool
}

allowed := slices.ContainsFunc(memberships, func(m Membership) bool {
    return m.UserID == userID && m.Active && m.Role == "admin"
})

The code says exactly what must be true for access to be allowed. If that condition becomes shared across several call sites, move it into a named function rather than duplicating an increasingly complicated closure:

func isActiveAdminFor(userID string) func(Membership) bool {
    return func(m Membership) bool {
        return m.UserID == userID && m.Active && m.Role == "admin"
    }
}

allowed := slices.ContainsFunc(memberships, isActiveAdminFor(userID))

There is a trade-off here. A small inline predicate keeps a local rule close to its use. A long predicate with parsing, normalization, logging, and multiple branches can make the search harder to understand than an ordinary loop. Extract the domain rule when naming it makes the code clearer.

Nil and empty slices simply return false

A nil slice has no elements, so there is nothing for the predicate to match:

var jobs []Job

found := slices.ContainsFunc(jobs, func(Job) bool {
    return true
})

fmt.Println(found) // false

The predicate isn’t called. An empty non-nil slice behaves the same way.

For an existence check, that’s usually the desired result. If your application distinguishes “not loaded” from “loaded but empty,” check nilness separately before using ContainsFunc. The helper answers a content question; it doesn’t preserve that higher-level state distinction in its boolean result.

Don’t use ContainsFunc to find the matching value

A common mistake is to capture the match in an outer variable:

var matched Job
found := slices.ContainsFunc(jobs, func(job Job) bool {
    if job.Name == target {
        matched = job
        return true
    }
    return false
})

This works mechanically, but it makes ContainsFunc responsible for two jobs: reporting existence and smuggling the matching value out through a side effect.

If the caller needs the element, slices.IndexFunc makes that requirement explicit:

i := slices.IndexFunc(jobs, func(job Job) bool {
    return job.Name == target
})
if i >= 0 {
    matched := jobs[i]
    // use matched
}

An explicit loop can be even better when you need to return a pointer, propagate an error from the matching logic, or collect more context. Choosing a boolean helper and then working around its boolean result usually signals that the operation isn’t really an existence check anymore.

Watch for expensive work inside the predicate

A ContainsFunc search is linear in the number of elements it has to inspect. The cost of the predicate is paid for each inspected element, so seemingly small choices inside it can dominate the search.

For instance, if every comparison needs the same normalized target, normalize that target once before calling ContainsFunc:

normalizedTarget := strings.ToLower(strings.TrimSpace(target))

found := slices.ContainsFunc(users, func(user User) bool {
    return strings.ToLower(strings.TrimSpace(user.Name)) == normalizedTarget
})

The per-user normalization still happens for each element, but the target isn’t repeatedly normalized. For more expensive transformations, consider storing normalized data ahead of time or building an index if the same slice is searched repeatedly.

Repeated membership checks are where a map often becomes a better representation. Scanning a small slice once is simple. Scanning a large, mostly unchanged slice for many keys can turn a clear helper call into unnecessary repeated work.

Use slices.ContainsFunc for a clean existence check

Reach for slices.ContainsFunc when the real question is “does any element satisfy this rule?” Keep the predicate small, expect it to stop at the first match, and remember that nil and empty slices both produce false without invoking the callback.

If you later need the index, the matching value, every match, or an error from the predicate, switch to a helper or loop that represents that richer requirement directly. The best use of ContainsFunc is the simple one: a custom condition in, one boolean answer out.