Question
In Go, is there a built-in way to do something like slice.contains(value) for a slice, without manually searching through each element?
For example, if I have a slice and want to know whether a specific value exists in it, do I need to iterate through the slice myself, or is there an alternative approach in Go?
Short Answer
By the end of this page, you will understand how membership checks work for slices in Go, why slices do not provide constant-time contains lookup, and when to use a loop, helper function, or a map instead.
Concept
In Go, a slice is an ordered collection of elements. It is designed for storing and iterating over sequences of values, not for fast membership lookup.
If you want to know whether a slice contains a value, the usual approach is to check each element until you find a match. This is called a linear search.
for _, v := range slice {
if v == target {
// found it
}
}
Why? Because a slice does not maintain any extra structure that tells Go where a value is located. The values are just stored in order. To know whether target exists, Go may need to inspect elements one by one.
Why this matters
Choosing the right data structure affects both clarity and performance:
- Use a slice when order matters or when you mainly iterate over items.
- Use a map when you need fast lookup like “does this value exist?”
For many small slices, a loop is perfectly fine and idiomatic. For repeated membership checks on larger datasets, a map[T]bool or map[T]struct{} is usually a better fit.
Built-in support
Modern Go includes helper functions in the standard library package slices for some common slice operations.
import "slices"
found := slices.Contains(items, target)
However, this still performs a search through the slice internally. It is more convenient, but not asymptotically faster than writing the loop yourself.
Mental Model
Think of a slice like a row of labeled boxes on a shelf.
If someone asks, “Is there a blue box here?”, and the boxes are not indexed by color, you must look at the boxes one by one until you find a blue one or reach the end.
A map is more like a catalog or index. Instead of checking every box, you ask the catalog directly whether a blue box exists.
So:
- Slice = a sequence you scan
- Map = a lookup table you query
That is why contains on a slice requires checking elements, while a map can answer much faster.
Syntax and Examples
Basic linear search with a loop
package main
import "fmt"
func contains(nums []int, target int) bool {
for _, n := range nums {
if n == target {
return true
}
}
return false
}
func main() {
nums := []int{10, 20, 30, 40}
fmt.Println(contains(nums, 30)) // true
fmt.Println(contains(nums, 99)) // false
}
This is the classic and idiomatic approach:
- loop through the slice
- compare each element to the target
- return
trueas soon as a match is found - return
falseif the loop finishes without finding one
Using the standard library slices.Contains
If you are using a recent Go version that includes the slices package:
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{10, 20, 30, 40}
fmt.Println(slices.Contains(nums, 30)) // true
fmt.Println(slices.Contains(nums, 99)) // false
}
This is shorter and more readable, but it still checks elements internally.
Using a map for repeated lookups
package main
import "fmt"
func main() {
nums := []int{10, 20, 30, 40}
lookup := make(map[int]struct{})
for _, n := range nums {
lookup[n] = struct{}{}
}
_, found30 := lookup[30]
_, found99 := lookup[99]
fmt.Println(found30) // true
fmt.Println(found99) // false
}
This takes some setup, but once the map is built, membership checks are much faster on average.
When to choose which approach
- One or a few lookups: use a loop or
slices.Contains - Many lookups: build a map first
Step by Step Execution
Consider this example:
nums := []int{5, 8, 13}
target := 8
found := false
for _, n := range nums {
if n == target {
found = true
break
}
}
Here is what happens step by step:
numsis created with three values:5,8, and13.targetis set to8.foundstarts asfalse.- The loop begins.
- First iteration:
nis5.5 == 8is false.- continue to the next element.
- Second iteration:
nis8.8 == 8is true.- set
found = true breakexits the loop early.
- The final value of
foundistrue.
If target were 99, the loop would check all elements and found would remain false.
Early return version
A helper function is often cleaner:
func contains(nums []int, target int) bool {
for _, n := range nums {
if n == target {
return true
}
}
return false
}
This avoids the extra found variable and immediately returns when the value is found.
Real World Use Cases
Checking whether a slice contains a value appears in many practical programs.
Common examples
- User roles: check whether a user has a role like
"admin" - Allowed file extensions: verify whether
".jpg"is in a list of accepted types - Feature flags: see whether a feature name exists in an enabled-features slice
- Input validation: confirm whether a submitted value is one of the permitted options
- Filtering data: keep or skip records based on membership in a known list
Example: validating allowed statuses
func isAllowedStatus(status string) bool {
allowed := []string{"pending", "approved", "rejected"}
for _, s := range allowed {
if s == status {
return true
}
}
return false
}
For a small fixed list, a slice is simple and readable.
Example: frequent API permission checks
If an API checks permissions many times per request, a map is often better:
permissions := map[string]struct{}{
"read": {},
"write": {},
"admin": {},
}
_, ok := permissions["write"]
This avoids repeatedly scanning a slice.
Real Codebase Usage
In real Go codebases, developers usually choose between convenience, readability, and lookup performance.
Common patterns
Guard clauses
Membership checks are often used to reject invalid input early.
func createUser(role string) error {
allowed := []string{"admin", "editor", "viewer"}
if !slices.Contains(allowed, role) {
return fmt.Errorf("invalid role: %s", role)
}
return nil
}
This is a guard clause: fail early before continuing.
Validation helpers
Teams often wrap membership logic in reusable helper functions.
func containsString(items []string, target string) bool {
for _, item := range items {
if item == target {
return true
}
}
return false
}
This keeps business logic clean.
Map-based lookup tables
If the same values are checked repeatedly, developers often convert slices to maps once.
func buildLookup(items []string) map[string]struct{} {
lookup := make(map[string]struct{}, len(items))
for _, item := range items {
lookup[item] = struct{}{}
}
return lookup
}
Configuration and allowlists
Maps are common for:
- allowed environment names
- supported locales
- blocked IP addresses
- enabled feature names
Performance mindset
Developers do not automatically replace every slice with a map. For small datasets, a simple scan is often easier to read and completely fast enough. A map is usually chosen only when lookup speed matters or checks happen frequently.
Common Mistakes
1. Expecting slices to support instant membership lookup
A slice does not store an index by value. This means checking membership requires scanning elements.
nums := []int{1, 2, 3}
// There is no built-in property of the slice itself that makes this O(1).
If you need fast repeated lookup, use a map.
2. Thinking slices.Contains is fundamentally faster
This is convenient:
found := slices.Contains(nums, 3)
But it still performs a linear search internally. It improves readability, not the algorithm.
3. Forgetting to stop early
Broken or less efficient version:
func contains(nums []int, target int) bool {
found := false
for _, n := range nums {
if n == target {
found = true
}
}
return found
}
This works, but it keeps scanning even after finding the match.
Better:
func contains(nums []int, target int) bool {
for _, n := range nums {
if n == target {
return true
}
}
return false
}
4. Using a slice when a map is the real requirement
If your code constantly asks “does this exist?”, a slice may be the wrong structure.
Broken design for frequent checks:
allowed := []string{"a", "b", "c"}
Better design:
allowed := map[string]struct{}{
"a": {},
"b": {},
"c": {},
}
5. Comparing non-comparable values directly
Some types cannot be compared with ==, such as slices.
Broken example:
// invalid if items is [][]int and target is []int
if items[0] == target {
// error
}
For complex values, you may need custom comparison logic or packages such as reflect in limited cases.
Comparisons
| Approach | Best for | Time per lookup | Notes |
|---|---|---|---|
Manual for loop | Simple one-off checks | O(n) | Idiomatic and explicit |
slices.Contains | Readable one-off checks | O(n) | Cleaner syntax, same search cost |
map[T]struct{} lookup | Many repeated checks | Average O(1) | Requires extra memory and setup |
| Sorted slice + binary search | Repeated checks on sorted data | O(log n) | Only works if data is sorted |
Slice vs map
| Feature | Slice | Map |
|---|---|---|
| Preserves order | Yes | No guaranteed order |
| Allows duplicates | Yes | Keys are unique |
| Fast membership lookup | No | Yes, on average |
| Good for iteration | Yes | Yes |
| Good for repeated existence checks | Usually no | Usually yes |
slices.Contains vs writing your own function
| Option | Advantage | Disadvantage |
|---|---|---|
slices.Contains | Short and expressive | Requires a Go version with the slices package |
Custom contains function | Works anywhere and can be customized | Slightly more code |
Cheat Sheet
Quick reference
Check whether a slice contains a value
for _, v := range items {
if v == target {
return true
}
}
return false
Standard library helper
import "slices"
found := slices.Contains(items, target)
Fast repeated lookups with a map
lookup := make(map[string]struct{})
for _, item := range items {
lookup[item] = struct{}{}
}
_, found := lookup[target]
Rules to remember
- Slices do not provide constant-time membership lookup.
- Checking a slice usually means a linear search.
slices.Containsis convenient, but still linear.- Use a map if you need lots of membership checks.
- Use
return trueimmediately when you find a match.
Edge cases
- Empty slice: always returns
false - Duplicates: finding one match is enough
- Non-comparable element types: cannot use
==directly - Sorted data: consider binary search if appropriate
FAQ
Is there a built-in contains method on slices in Go?
No. Slices do not have methods like slice.contains(...). In modern Go, you can use slices.Contains, but it is a function from the standard library, not a method on the slice itself.
Do I always need to loop through a slice to check whether it contains a value?
Yes, unless you use another data structure such as a map, or your data is sorted and you use binary search. A regular slice membership check is a linear scan.
Is slices.Contains faster than a for loop in Go?
Not in algorithmic terms. Both are linear-time operations. slices.Contains is mainly a readability and convenience improvement.
When should I use a map instead of a slice in Go?
Use a map when your main question is “does this value exist?” and you need to ask it often. Use a slice when order matters or when you mainly iterate through values.
Can I use == to compare any slice element type?
No. The element type must be comparable. For example, int and string are comparable, but slices themselves are not.
What if my slice is sorted?
If the slice is sorted, you can use binary search for faster lookups than a linear scan. That is useful when you want repeated searches and your data can stay sorted.
Mini Project
Description
Build a small Go program that validates whether user-provided tags are allowed. This demonstrates both ways to perform membership checks: scanning a slice and using a map for faster repeated lookups.
Goal
Create a program that checks whether given tags exist in an allowed list and prints whether each tag is valid.
Requirements
- Create a slice of allowed tags such as
"go","api", and"backend". - Write a helper function that checks whether a slice contains a string.
- Test the helper with several sample tags and print the results.
- Build a map from the same tags and perform the same checks using map lookup.
- Show that both approaches return the same answers.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.