Golang Slice Contains and Find Index: Contains, Index, and IndexFunc

Learn how to check if a Go slice contains a value, find the index of an element, search by condition with IndexFunc, and choose between slices, loops, and maps.

Published

Updated

Read time 15 min read

Reviewed byDeepak Prasad

Golang Slice Contains and Find Index: Contains, Index, and IndexFunc

Most people who land on “search in a slice” actually want one of a few concrete outcomes: see if a value is present, get its index, or find the first element that satisfies a rule (for example a struct field or a string prefix). In modern Go, the standard library slices package answers the first two with slices.Contains and slices.Index, and condition-based lookups with slices.IndexFunc and slices.ContainsFunc. This guide walks through each pattern, when to use a loop instead, how arrays relate to slices, and when a map is a better data structure for repeated lookups. For control flow basics, see the for loop in Go tutorial.

Go 1.21 or newer on Linux for the slices examples; older toolchains can use the manual loop patterns shown later.


What does it mean to search a slice in Go?

“Search” is overloaded: you might mean membership, first index, first custom match, or every match. Start by naming which outcome you need, then pick a helper from the table.

Map your goal to a helper

You want Good first choice
Check if a value exists slices.Contains
Index of an exact value slices.Index
First index where a rule holds slices.IndexFunc
Whether any element matches a rule slices.ContainsFunc
Every matching index or value for range loop (collect results yourself)
Many repeated lookups by key map

Example: four common answers in one program

go
package main

import (
	"fmt"
	"slices"
	"strings"
)

func main() {
	nums := []int{10, 20, 30}
	fmt.Println("Contains 20:", slices.Contains(nums, 20))
	fmt.Println("Index 99:", slices.Index(nums, 99))

	words := []string{"foo", "bar", "baz"}
	fmt.Println("IndexFunc prefix ba:", slices.IndexFunc(words, func(s string) bool {
		return strings.HasPrefix(s, "ba")
	}))
	fmt.Println("ContainsFunc has z:", slices.ContainsFunc(words, func(s string) bool {
		return strings.Contains(s, "z")
	}))
}
Output

When you run it, you should see Contains 20: true, Index 99: -1 for a missing value, IndexFunc printing 1 for the first string with prefix ba (bar), and ContainsFunc has z: true because of baz.

How the rest of this guide is organized

The following sections go deeper in the same order: exact membership (Contains), exact index (Index), predicates (IndexFunc and ContainsFunc), struct-heavy workflows, collecting every match, manual loops for older Go or complex logic, arrays as slice views, maps for hot lookups, and sorted data with binary search. Skim the cheat sheet near the end if you already know which row you need.


Check if a slice contains a value

slices.Contains answers “is this value present?” using == on whole elements. It walks the slice from the start and stops at the first match. Use it for strings, integers, and other comparable types—including structs when you truly mean “same complete value,” not “same ID field.”

Example: membership in a []string (tags)

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	tags := []string{"linux", "go", "deb"}
	fmt.Println(slices.Contains(tags, "go"))
	fmt.Println(slices.Contains(tags, "win"))
}
Output

You should see true then false.

Example: membership in a []int

Same API for numeric slices:

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	levels := []int{100, 200, 300}
	fmt.Println(slices.Contains(levels, 200))
	fmt.Println(slices.Contains(levels, 400))
}
Output

You should see true then false.

Example: comparable structs (whole value, not one field)

Contains compares the entire struct. This fits tiny config rows or tokens where equality really is “same name string in the struct.”

go
package main

import (
	"fmt"
	"slices"
)

type Tag struct {
	Name string
}

func main() {
	row := []Tag{{"go"}, {"rust"}, {"zig"}}
	fmt.Println(slices.Contains(row, Tag{"go"}))
	fmt.Println(slices.Contains(row, Tag{"java"}))
}
Output

You should see true then false. If you only care about one field (for example user ID), use slices.IndexFunc or slices.ContainsFunc from the sections below instead of Contains.


Find the index of an element in a slice

slices.Index returns the index of the first element equal to the needle, or -1 if nothing matches. Always treat -1 as “not found” before using the index with slice[i].

Example: first match when duplicates exist

With duplicates, Index only reports the first position:

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	scores := []int{40, 55, 55, 90}
	fmt.Println(slices.Index(scores, 55))
}
Output

You should see 1 (the first 55 only).

Example: missing value returns -1

When the value is absent, the result is -1, not 0 and not the last index:

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	ids := []int{10, 20, 30}
	fmt.Println(slices.Index(ids, 99))
}
Output

You should see -1.

Example: safe lookup before using the index

Guard the result, then read or update the element:

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	labels := []string{"alpha", "beta", "gamma"}
	if i := slices.Index(labels, "beta"); i >= 0 {
		fmt.Println("at", i, "value", labels[i])
	}
}
Output

You should see at 1 value beta.


Find an element by condition

slices.IndexFunc takes func(T) bool, returns the first index where the predicate is true, or -1 if none match. It is the usual answer for “slices.indexfunc” style lookups: prefixes, thresholds, or one field on a struct. The return value is an index, not the element—read slice[idx] after checking idx != -1.

Example: slices.IndexFunc with strings (prefix / custom test)

go
package main

import (
	"fmt"
	"slices"
	"strings"
)

func main() {
	words := []string{"foo", "bar", "baz"}
	i := slices.IndexFunc(words, func(s string) bool {
		return strings.HasPrefix(s, "ba")
	})
	fmt.Println(i)
}
Output

You should see 1 (bar is the first match).

Example: slice of structs (find user by ID)

go
package main

import (
	"fmt"
	"slices"
)

type User struct {
	ID   int
	Name string
}

func main() {
	users := []User{{2, "Ann"}, {5, "Bob"}, {9, "Cam"}}
	idx := slices.IndexFunc(users, func(u User) bool { return u.ID == 5 })
	if idx >= 0 {
		fmt.Println(users[idx].Name)
	}
}
Output

You should see Bob.

Example: predicate that closes over a variable

Use a local variable inside main when the threshold or target ID is not hard-coded:

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	values := []int{3, 7, 2, 9}
	limit := 6
	idx := slices.IndexFunc(values, func(n int) bool { return n > limit })
	fmt.Println(idx, values[idx])
}
Output

You should see 1 and 7 (first value greater than limit).


Check a condition without needing the index

slices.ContainsFunc is the boolean cousin of IndexFunc: same predicate, but you only learn whether any element matched. Both stop at the first success.

Example: any element matches a rule

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	nums := []int{1, 3, 5, 8}
	fmt.Println(slices.ContainsFunc(nums, func(n int) bool { return n%2 == 0 }))
}
Output

You should see true because 8 is even.

Example: same predicate with ContainsFunc vs IndexFunc

One answers “any?”; the other answers “where first?”:

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	nums := []int{2, 4, 6, 8}
	p := func(n int) bool { return n > 5 }
	fmt.Println("ContainsFunc:", slices.ContainsFunc(nums, p))
	fmt.Println("IndexFunc:", slices.IndexFunc(nums, p))
}
Output

You should see ContainsFunc: true and IndexFunc: 2 (first element greater than 5 is 6 at index 2).

Example: “any negative?” style boolean check

When you only need yes or no (for example validation), ContainsFunc keeps the call site simple:

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	temps := []int{12, 18, 22, 4}
	fmt.Println(slices.ContainsFunc(temps, func(t int) bool { return t < 0 }))
}
Output

You should see false for this data set.


Search a slice of structs

Structs add two different questions: do you care about the entire row (== on every field), or about a stable business key (ID, SKU, email)? The standard helpers behave differently depending on which question you ask.

Example: whole-row equality with Index and Contains

slices.Index and slices.Contains use == on the full struct value. Every comparable field must match.

go
package main

import (
	"fmt"
	"slices"
)

type Item struct {
	ID   int
	Name string
}

func main() {
	catalog := []Item{{1, "a"}, {2, "b"}}
	fmt.Println(slices.Index(catalog, Item{2, "b"}))
	fmt.Println(slices.Contains(catalog, Item{2, "x"}))
}
Output

You should see 1 then false (same ID but different name is not equal to {2, "b"}).

Example: match one field with IndexFunc (SKU / ID style)

When several rows might share a key or you only care about one column, search with a predicate instead of whole-value equality.

go
package main

import (
	"fmt"
	"slices"
)

type Product struct {
	SKU  string
	Name string
}

func main() {
	catalog := []Product{{"A1", "Apple"}, {"B2", "Banana"}, {"B2", "Plantain"}}
	i := slices.IndexFunc(catalog, func(p Product) bool { return p.SKU == "B2" })
	fmt.Println(i, catalog[i].Name)
}
Output

You should see 1 BananaIndexFunc returns the first row whose SKU is B2, even though another B2 row exists later.


Find all matching elements in a slice

slices.Index, slices.IndexFunc, slices.Contains, and slices.ContainsFunc only report the first hit (or a boolean). Collecting every match is still a loop problem.

Example: collect every index with the same value

go
package main

import "fmt"

func main() {
	nums := []int{2, 4, 4, 6, 4}
	var hits []int
	for i, v := range nums {
		if v == 4 {
			hits = append(hits, i)
		}
	}
	fmt.Println(hits)
}
Output

You should see [1 2 4].

Example: collect every value that passes a rule

go
package main

import "fmt"

func main() {
	words := []string{"aa", "aba", "bb", "abc"}
	var out []string
	for _, w := range words {
		if len(w) >= 2 && w[0] == 'a' {
			out = append(out, w)
		}
	}
	fmt.Println(out)
}
Output

You should see [aa aba abc].

Example: first match from Index vs all indices from a loop

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	nums := []int{2, 4, 4, 6}
	fmt.Println("first index of 4:", slices.Index(nums, 4))
	var all []int
	for i, v := range nums {
		if v == 4 {
			all = append(all, i)
		}
	}
	fmt.Println("all indices:", all)
}
Output

You should see first index of 4: 1 then all indices: [1 2], showing why you still reach for for range when the question is “everywhere,” not “first.”


Manual search with for range

Before Go 1.21—or whenever the predicate needs multiple steps, logging, or custom return values—a straight for range loop stays the most flexible tool.

Example: findIndex and contains helpers

go
package main

import "fmt"

func findIndex(haystack []string, needle string) int {
	for i, v := range haystack {
		if v == needle {
			return i
		}
	}
	return -1
}

func contains(haystack []string, needle string) bool {
	return findIndex(haystack, needle) >= 0
}

func main() {
	words := []string{"this", "is", "a", "very", "simple", "example"}
	fmt.Println(contains(words, "this"), findIndex(words, "this"))
	fmt.Println(contains(words, "that"), findIndex(words, "that"))
}
Output

You should see true and 0 for this, then false and -1 for that.

Example: loop with extra conditions

Predicates inside IndexFunc must stay simple; loops stay readable when you combine length checks, character tests, and early break semantics.

go
package main

import "fmt"

func main() {
	xs := []string{"x", "yy", "zzz", "a"}
	for i, s := range xs {
		if len(s) >= 2 && s[0] == 'y' {
			fmt.Println(i, s)
			break
		}
	}
}
Output

You should see 1 yy.

Example: older toolchains and custom return shapes

If you cannot import slices yet, keep tiny helpers like findIndex in an internal package and unit-test them. Loops also let you return (index, value, ok) tuples or accumulate errors without fighting closure signatures.


Search in array vs slice

Go distinguishes arrays ([N]T, fixed size) from slices ([]T, dynamic length). The slices helpers expect a slice argument, so array workflows almost always start with a slice view.

Example: Contains on an array via arr[:]

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	arr := [4]int{10, 20, 30, 40}
	view := arr[:]
	fmt.Println(slices.Contains(view, 20))
}
Output

You should see true.

Example: Index on the same array view

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	arr := [4]int{10, 20, 30, 40}
	view := arr[:]
	fmt.Println(slices.Index(view, 30))
	fmt.Println(slices.Index(view, 99))
}
Output

You should see 2 then -1.

Example: pass arr[:] into helpers that take []T

go
package main

import (
	"fmt"
	"slices"
)

func sumFirstTwo(values []int) int {
	if len(values) < 2 {
		return 0
	}
	return values[0] + values[1]
}

func main() {
	arr := [3]int{1, 2, 3}
	fmt.Println(sumFirstTwo(arr[:]))
	fmt.Println(slices.Contains(arr[:], 2))
}
Output

You should see 3 then true. The slice view shares backing storage with the array; it does not copy elements.


When to use a map instead of searching a slice

Linear helpers scan elements until they succeed or exhaust the slice. That is fine for small data or rare queries, but repeated membership checks on large slices push you toward indexing structures.

Example: repeated membership checks on a slice

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	ids := []int{1, 5, 9, 12}
	queries := []int{5, 5, 5, 1, 12}
	for _, q := range queries {
		_ = slices.Contains(ids, q) // each call scans up to len(ids)
	}
	fmt.Println("linear scans:", len(queries))
}
Output

You should see linear scans: 5. Each line is simple, but the work grows with len(ids) * len(queries).

Example: build a set map once, then answer many queries

go
package main

import "fmt"

func main() {
	ids := []string{"alfa", "bravo", "charlie"}
	set := make(map[string]struct{}, len(ids))
	for _, id := range ids {
		set[id] = struct{}{}
	}
	_, okBravo := set["bravo"]
	_, okDelta := set["delta"]
	fmt.Println(okBravo, okDelta)
}
Output

You should see true false. Lookups become O(1) on average after the O(n) build.

Slice vs map decision table

Situation Prefer
Small slice, occasional lookup Slice search
Need original order Slice search
Many repeated lookups by key Map
Checking membership often Map
Need all matching values Loop over slice
Sorted data, many probes Binary search (below)

Sorted slices: slices.BinarySearch and sort.Search

Binary search answers “where does this key sit in sorted data?” in logarithmic time, but only when the slice is sorted ascending (for slices.BinarySearch) or when your sort.Search predicate matches how you sorted.

Example: slices.BinarySearch on sorted ints and strings

go
package main

import (
	"fmt"
	"slices"
)

func main() {
	sorted := []int{1, 2, 3, 4, 5, 6, 7}
	if i, ok := slices.BinarySearch(sorted, 4); ok {
		fmt.Println("found 4 at", i)
	} else {
		fmt.Println("not found, would insert at", i)
	}

	names := []string{"Anna", "Bang", "Bengi", "Daniel", "Faker"}
	i, ok := slices.BinarySearch(names, "Daniel")
	fmt.Println("Daniel:", i, ok)
}
Output

You should see found 4 at 3 and Daniel: 3 true for the sample data.

Example: sort.Search after sort.Slice

When you sorted with sort.Slice, locate the first index whose element is >= a key using sort.Search, then compare the element at that index for a true membership test.

go
package main

import (
	"fmt"
	"sort"
)

func main() {
	strSlice := []string{"Daniel", "Anna", "Bengi", "Faker", "Bang"}
	sort.Slice(strSlice, func(i, j int) bool {
		return strSlice[i] <= strSlice[j]
	})

	for _, key := range []string{"Clair", "Anna"} {
		idx := sort.Search(len(strSlice), func(i int) bool {
			return key <= strSlice[i]
		})
		if idx < len(strSlice) && strSlice[idx] == key {
			fmt.Printf("Found %q at %d in %v\n", key, idx, strSlice)
		} else {
			fmt.Printf("Not found %q in %v\n", key, strSlice)
		}
	}
}
Output

You should see Clair not found in the sorted slice and Anna found at index 0.

If the slice is not sorted, slices.BinarySearch results are meaningless for membership. Sort first (and keep the sorted order stable), or stay with linear Index / IndexFunc scans.


Contains vs Index vs IndexFunc vs ContainsFunc

Use this matrix when you know the shape of the question (“exact value?”, “predicate?”, “every match?”). Pair it with the longer sections above for full examples.

Quick reference matrix

Need Use
Check exact value exists slices.Contains
Find index of exact value slices.Index
Check if any item matches a condition slices.ContainsFunc
Find index by condition slices.IndexFunc
Find all matches for range loop
Search an array Convert with arr[:] then use slices
Many repeated lookups map
Sorted slice lookup slices.BinarySearch (or sort.Search after sorting)

When you leave the slices helpers

Collecting every hit, preserving insertion order while indexing, or joining related tables still wants explicit loops. Hot membership paths should build a map once. Ordered collections with many probes belong on sorted slices plus slices.BinarySearch or sort.Search—see the dedicated sections for runnable samples.

Pairs that confuse newcomers

Contains answers bool; Index answers position or -1. ContainsFunc answers bool for arbitrary tests; IndexFunc answers the first index for the same style of test. None of them return a value directly, and none enumerate all matches.


Common mistakes

These issues show up often when mixing search terminology from other languages or assuming slices helpers behave like full query engines.

Vague “golang search” questions and binary search misuse

“Search” might mean files, SQL, substring work, or maps—not slice membership. Pick the concrete outcome first. Likewise, slices.BinarySearch only applies after data is sorted; running it on arbitrary order is a silent logic bug, not a faster Index.

Mishandling -1 and IndexFunc results

Index and IndexFunc return -1 when nothing matches—never index blindly. They also return an int index, not the element itself, and they stop at the first match instead of listing every row.

go
xs := []int{1, 2, 3}
idx := slices.Index(xs, 99)
if idx >= 0 {
	_ = xs[idx] // only safe after the guard
}

Using Contains when a field defines identity

Contains compares the whole struct. Matching “same product SKU” or “same user ID” needs ContainsFunc / IndexFunc predicates, plus loops when you must return every duplicate SKU.


Go slice search cheat sheet

Keep this nearby while coding: it mirrors the larger tables earlier but compresses everything to a single scan-friendly view.

Core slices calls

Goal Use
Check if exact value exists slices.Contains
Find index of exact value slices.Index
Check if any item matches a condition slices.ContainsFunc
Find index by condition slices.IndexFunc

Collections, adapters, and older Go

Goal Use
Find all matching values for range loop
Support older Go versions Manual loop
Search an array arr[:] then slices

Performance-shaped choices

Goal Use
Many repeated lookups map
Sorted slice lookup slices.BinarySearch

Summary

Checking if a Go slice contains a value is slices.Contains; finding the first index of an exact value is slices.Index, always treating -1 as not found. Condition-based checks use slices.ContainsFunc (boolean) and slices.IndexFunc (first index or -1), which fit struct fields and custom rules better than whole-value equality. Collect every match with a loop; switch to a map when you repeat lookups often enough that linear scans hurt. For sorted data, use slices.BinarySearch or sort.Search after sorting instead of scanning the whole slice each time. Arrays can use the same helpers via a slice view arr[:].


References


Frequently Asked Questions

1. How do I check if a slice contains a value in Go?

Use slices.Contains from the standard library slices package (Go 1.21+). It returns true if any element equals the value and false otherwise; it does not return an index.

2. What does slices.Index return when the value is missing?

It returns -1. Always compare the result to -1 before indexing the slice, otherwise you may read the wrong element or panic on an empty slice depending on use.

3. When should I use slices.IndexFunc instead of slices.Index?

Use IndexFunc when equality on the whole value is not what you need—for example matching a struct field, a numeric threshold, or a string prefix. Index is for exact comparable matches only.

4. What is the difference between slices.ContainsFunc and slices.IndexFunc?

ContainsFunc returns only a bool (whether any element satisfies the predicate). IndexFunc returns the index of the first match or -1. Both stop at the first match.

5. Should I use golang.org/x/exp/slices or import "slices"?

Prefer the standard library slices package from Go 1.21 onward. x/exp/slices was the older experimental path; new code should use std slices unless a legacy module cannot upgrade.

6. Does slices.BinarySearch require a sorted slice?

Yes. If the slice is not sorted in ascending order, the index and found flag are not meaningful for arbitrary data.
Tuan Nguyen

Data Scientist

Proficient in Golang, Python, Java, MongoDB, Selenium, Spring Boot, Kubernetes, Scrapy, API development, Docker, Data Scraping, PrimeFaces, Linux, Data Structures, and Data Mining. With expertise …