How to check if a slice contains an element in Go
On Go 1.21+, use the standard library: slices.Contains(s, v) returns true if any element equals v. On older Go, write the three-line for … range loop — there is no s.Contains(v) method on slices.
For its first thirteen years Go famously made you write a loop for this — the most-asked slice question. Since Go 1.21 the answer is one call: the generic slices package ships Contains, ContainsFunc, and Index, and they work on a slice of any comparable type. This page covers all of them, plus the loop you'll still meet in older codebases and the point where a slice is the wrong data structure entirely. Each example runs on this page — hit Run, then edit the code and run it again.
1slices.Contains (Go 1.21+)Recommended
slices.Contains(s, v) walks the slice and reports whether any element == v. It's generic, so the same call works for strings, ints, floats — any comparable element type — and it reads exactly like what it does. This is the idiomatic form in any codebase on Go 1.21 or newer.
Output
Prints true, false, then 7 is prime. Calling it on a nil or empty slice is safe and simply returns false — no guard needed. One near-namesake to not confuse it with: strings.Contains checks for a substring inside one string, not membership in a slice.
2The classic loop (any Go version)
Before Go 1.21 this loop was the answer, and it's still what slices.Contains does under the hood: range the slice, compare, return early on a hit. You'll see a helper like this in virtually every pre-generics codebase — often once per element type, which is exactly the boilerplate the slices package erased.
Output
Prints true then false. The early return true matters: on average you scan half the slice for a hit, all of it for a miss — the same O(n) cost slices.Contains has. Inline the loop where it's used once; extract the helper when you check membership in more than one place.
3Custom matches with slices.ContainsFunc
== is often the wrong test: you want "equal ignoring case", or "any struct whose field matches". slices.ContainsFunc(s, f) takes a predicate and reports whether it returns true for any element — the escape hatch for every comparison slices.Contains can't express.
Output
Prints true then false — "Go" matches "GO" under strings.EqualFold (proper Unicode case folding, cheaper than lower-casing both sides), and neither user is under 18. Sibling helpers slices.IndexFunc and slices.Max/MinFunc take the same predicate shape.
4When you need the position: slices.Index
Often "is it there?" is really "where is it?" — you want to update, remove, or slice around the element. slices.Index(s, v) returns the index of the first match, or -1 if there is none, so one call answers both questions.
Output
Prints 2, -1, then banana at index 1. The if i := …; i >= 0 form scopes the index to the branch that uses it — the same pattern as the map comma-ok idiom. Note slices.Contains(s, v) is literally defined as slices.Index(s, v) >= 0.
5Which should you use?
| Form | Needs | Best for |
|---|---|---|
| slices.Contains(s, v) | Go 1.21+ | Yes/no membership — the default |
| slices.ContainsFunc(s, f) | Go 1.21+ | Case-insensitive, structs, any custom test |
| slices.Index(s, v) | Go 1.21+ | You also need the position |
| for … range loop | Any version | Pre-1.21 codebases |
| map[T]struct{} set | Any version | Many lookups against the same data |
6Many lookups? Use a map, not a slice
Every form above scans the slice — O(n) per call. Fine once; wasteful inside a loop that checks hundreds of values against the same list. The Go idiom for that is to pay O(n) once, building a map[T]struct{} set, and then answer every membership question in O(1) with the comma-ok lookup.
Output
Prints mail.com is fine then junk.net is blocked. The crossover comes fast: even a few dozen lookups against a moderate slice usually favors the map. The set pattern — empty-struct values, comma-ok reads — is covered in depth in check if a key exists in a map.
Frequently asked questions
Does Go have a built-in contains function for slices?
Yes, since Go 1.21: slices.Contains(s, v) in the standard library returns true if any element of the slice equals v. It is generic, so one function covers strings, ints, and any other comparable element type. Before 1.21 there was no built-in — you wrote a for-range loop.
How do I check if a slice contains a value before Go 1.21?
Write the loop: for _, v := range s { if v == target { return true } } and return false after it. That is exactly what slices.Contains does internally, so upgrading later is a mechanical find-and-replace.
How do I do a case-insensitive contains on a []string?
Use slices.ContainsFunc with strings.EqualFold: slices.ContainsFunc(s, func(v string) bool { return strings.EqualFold(v, target) }). EqualFold does Unicode case folding directly, which is both more correct and cheaper than lower-casing every element.
Does slices.Contains work on a slice of structs?
Yes, if every field of the struct is comparable — slices.Contains(users, User{"alice", 30}) compares field by field. To match on just one field (say, any user with a given name), use slices.ContainsFunc with a predicate instead. Structs containing slices, maps, or functions are not comparable and require the Func form.
Is slices.Contains slow for big slices?
It is a linear scan — O(n) per call — which is fine for a one-off check on any realistic slice. The trap is calling it repeatedly against the same slice, e.g. inside a loop: that is O(n×m). Build a map[T]struct{} set once and use the O(1) comma-ok lookup instead. If the slice is sorted, slices.BinarySearch gives O(log n) without the extra memory.
What is the difference between slices.Contains and strings.Contains?
Different questions entirely: slices.Contains([]string{...}, "go") asks whether a slice has an element equal to "go", while strings.Contains("golang", "go") asks whether one string contains another as a substring. Reaching for the wrong one is a common early mistake since both read as "contains".