Code often needs a simple membership check: is this status allowed, does this short list contain a requested format, or has this identifier already appeared in a small batch? slices.Contains answers that question directly for comparable values and returns a boolean.
The operation is deliberately narrow. It checks exact equality, doesn’t return a position, and scans the slice until it finds a match or reaches the end. Those details make it a good fit for some lookups and a poor fit for others.
Check membership with slices.Contains
Pass the slice and the value you want to find:
package main
import (
"fmt"
"slices"
)
func main() {
states := []string{"queued", "active", "done"}
fmt.Println(slices.Contains(states, "active"))
fmt.Println(slices.Contains(states, "paused"))
}The output is:
true
falseThe result expresses the caller’s intent without exposing an index that isn’t needed. The slice is not modified, so the same data can continue to be used in its original order.
This is especially convenient for small validation sets:
allowed := []string{"json", "csv", "text"}
if !slices.Contains(allowed, format) {
return fmt.Errorf("unsupported format %q", format)
}For a short list checked occasionally, that keeps the validation close to the values it accepts.
slices.Contains uses exact equality
The element type must be comparable. Strings, integers, booleans, pointers, channels, and structs containing only comparable fields can be checked directly.
A struct comparison includes every field:
type Key struct {
Region string
ID int
}
keys := []Key{
{Region: "apac", ID: 10},
{Region: "eu", ID: 20},
}
fmt.Println(slices.Contains(keys, Key{Region: "eu", ID: 20})) // true
fmt.Println(slices.Contains(keys, Key{Region: "apac", ID: 20})) // falseThat second lookup doesn’t match even though the ID is the same. Exact equality considers Region too.
Types containing slices, maps, or functions aren’t comparable, so a slice of such values can’t be passed to slices.Contains. When matching depends on one field or another rule, slices.ContainsFunc is the more suitable operation:
type Job struct {
ID int
Tags []string
}
jobs := []Job{
{ID: 10, Tags: []string{"batch"}},
{ID: 20, Tags: []string{"urgent"}},
}
found := slices.ContainsFunc(jobs, func(job Job) bool {
return job.ID == 20
})
fmt.Println(found) // trueThe predicate makes the matching rule visible instead of forcing a type into exact equality that doesn’t represent the requirement.
Empty and nil slices simply return false
No guard is needed before checking an empty slice:
var nilStates []string
emptyStates := []string{}
fmt.Println(slices.Contains(nilStates, "active")) // false
fmt.Println(slices.Contains(emptyStates, "active")) // falseBoth inputs contain no matching element, so both calls return false. This is useful in code where an earlier parsing or filtering stage can legitimately produce no values.
A separate len check still makes sense when the application assigns a distinct meaning to an empty collection. For membership alone, it adds no information.
Exact floating-point checks can be surprising
Floating-point values are comparable, so they satisfy the type requirement. Exact equality still carries the usual floating-point semantics.
In particular, a NaN value doesn’t compare equal to itself:
values := []float64{1.5, math.NaN(), 3.5}
fmt.Println(slices.Contains(values, math.NaN())) // falseThat example requires an additional import:
import "math"If the application needs to detect NaN values, use a predicate:
hasNaN := slices.ContainsFunc(values, math.IsNaN)
fmt.Println(hasNaN) // trueApproximate numeric matching also belongs in a predicate. slices.Contains should not be treated as an epsilon-based floating-point search.
Use slices.Index when the position matters
A common mistake is to call slices.Contains and then perform another search to obtain the index:
if slices.Contains(states, target) {
index := slices.Index(states, target)
fmt.Println(index)
}That can scan the same slice twice. If the position is needed, call slices.Index once:
index := slices.Index(states, target)
if index != -1 {
fmt.Println(index)
}Use slices.Contains when a boolean is the actual result. Use slices.Index when the first position is part of the result. Choosing the narrower operation avoids extra work and makes the call site easier to read.
Repeated membership checks may call for a map
slices.Contains performs a linear search. For short slices and occasional checks, building another data structure is often unnecessary.
Repeated membership checks against a larger, stable collection have a different cost profile. A set-style map can move the work into a one-time setup step:
allowed := []string{"json", "csv", "text"}
allowedSet := make(map[string]struct{}, len(allowed))
for _, format := range allowed {
allowedSet[format] = struct{}{}
}
_, ok := allowedSet["csv"]
fmt.Println(ok) // trueThe map costs extra memory and setup, but subsequent membership checks use keyed lookup instead of rescanning the slice. It also discards ordering as part of the membership structure.
Don’t convert every small slice into a map by default. If the list has three values and is checked once, the slice version is simpler. A map becomes more attractive as lookup frequency grows or the collection is already represented as keyed data elsewhere in the program.
Keep membership checks matched to the data
slices.Contains fits exact yes-or-no membership checks over comparable slice elements. It handles nil and empty input without special cases and leaves the source slice untouched.
Switch operations when the requirement changes. Use slices.ContainsFunc for predicate-based matching, slices.Index when the first position matters, or a map when repeated membership checks justify maintaining a keyed structure. The best choice is the one that returns exactly the information the caller needs without adding a second search or an unnecessary data structure.