Question

How to Check Whether a Slice Contains a Value in Go

goslice

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.

Take Quiz

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 true as soon as a match is found
  • return false if 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:

  1. nums is created with three values: 5, 8, and 13.
  2. target is set to 8.
  3. found starts as false.
  4. The loop begins.
  5. First iteration: n is 5.
    • 5 == 8 is false.
    • continue to the next element.
  6. Second iteration: n is 8.
    • 8 == 8 is true.
    • set found = true
    • break exits the loop early.
  7. The final value of found is true.

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.

Take Quiz

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

ApproachBest forTime per lookupNotes
Manual for loopSimple one-off checksO(n)Idiomatic and explicit
slices.ContainsReadable one-off checksO(n)Cleaner syntax, same search cost
map[T]struct{} lookupMany repeated checksAverage O(1)Requires extra memory and setup
Sorted slice + binary searchRepeated checks on sorted dataO(log n)Only works if data is sorted

Slice vs map

FeatureSliceMap
Preserves orderYesNo guaranteed order
Allows duplicatesYesKeys are unique
Fast membership lookupNoYes, on average
Good for iterationYesYes
Good for repeated existence checksUsually noUsually yes

slices.Contains vs writing your own function

OptionAdvantageDisadvantage
slices.ContainsShort and expressiveRequires a Go version with the slices package
Custom contains functionWorks anywhere and can be customizedSlightly more code
Take Quiz

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.Contains is convenient, but still linear.
  • Use a map if you need lots of membership checks.
  • Use return true immediately 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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more