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
slicesexamples; 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
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")
}))
}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)
package main
import (
"fmt"
"slices"
)
func main() {
tags := []string{"linux", "go", "deb"}
fmt.Println(slices.Contains(tags, "go"))
fmt.Println(slices.Contains(tags, "win"))
}You should see true then false.
Example: membership in a []int
Same API for numeric slices:
package main
import (
"fmt"
"slices"
)
func main() {
levels := []int{100, 200, 300}
fmt.Println(slices.Contains(levels, 200))
fmt.Println(slices.Contains(levels, 400))
}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.”
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"}))
}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:
package main
import (
"fmt"
"slices"
)
func main() {
scores := []int{40, 55, 55, 90}
fmt.Println(slices.Index(scores, 55))
}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:
package main
import (
"fmt"
"slices"
)
func main() {
ids := []int{10, 20, 30}
fmt.Println(slices.Index(ids, 99))
}You should see -1.
Example: safe lookup before using the index
Guard the result, then read or update the element:
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])
}
}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)
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)
}You should see 1 (bar is the first match).
Example: slice of structs (find user by ID)
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)
}
}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:
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])
}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
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 }))
}You should see true because 8 is even.
Example: same predicate with ContainsFunc vs IndexFunc
One answers “any?”; the other answers “where first?”:
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))
}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:
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 }))
}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.
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"}))
}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.
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)
}You should see 1 Banana—IndexFunc 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
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)
}You should see [1 2 4].
Example: collect every value that passes a rule
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)
}You should see [aa aba abc].
Example: first match from Index vs all indices from a loop
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)
}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
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"))
}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.
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
}
}
}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[:]
package main
import (
"fmt"
"slices"
)
func main() {
arr := [4]int{10, 20, 30, 40}
view := arr[:]
fmt.Println(slices.Contains(view, 20))
}You should see true.
Example: Index on the same array view
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))
}You should see 2 then -1.
Example: pass arr[:] into helpers that take []T
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))
}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
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))
}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
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)
}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
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)
}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.
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)
}
}
}You should see Clair not found in the sorted slice and Anna found at index 0.
Unsorted data and binary search
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.
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
- Package slices
- Package sort
- For loop in Go
- Linear search (Wikipedia)
- Stack Overflow: search an element in a Go slice

