A slice often holds a short sequence where you need the position of one exact value: a status in a workflow, a command-line argument, a feature name, or an ID in a small ordered list. slices.Index handles that case directly. It returns the first matching index, or -1 when the value isn’t present.

That return contract is simple, but it affects how callers should use the result. Indexing the slice before checking for -1 will panic, repeated searches still scan linearly, and exact equality isn’t suitable for every data type or matching rule.

Find the first exact value with slices.Index

For strings, integers, and other comparable values, pass the slice and target directly:

package main

import (
    "fmt"
    "slices"
)

func main() {
    states := []string{"queued", "active", "done"}

    index := slices.Index(states, "active")

    fmt.Println(index)
    fmt.Println(states[index])
}

The output is:

1
active

slices.Index checks elements from the start of the slice and returns as soon as it finds an equal value. The slice isn’t reordered or otherwise modified.

If a value occurs more than once, only the first position is returned:

codes := []int{200, 404, 200, 500}

fmt.Println(slices.Index(codes, 200)) // 0

That behavior is useful when the earliest occurrence has meaning. If the task requires every matching position, an explicit loop is a better fit.

Check for -1 before indexing the slice

A missing value produces -1:

states := []string{"queued", "active", "done"}

index := slices.Index(states, "paused")
fmt.Println(index) // -1

Don’t use that result as a slice index until you’ve checked it. This code panics:

index := slices.Index(states, "paused")
fmt.Println(states[index])

A safe branch keeps absence explicit:

index := slices.Index(states, target)
if index == -1 {
    fmt.Printf("%q is not present\n", target)
    return
}

fmt.Printf("%q is at index %d\n", target, index)

If the caller only needs a yes-or-no answer, slices.Contains is more direct:

if slices.Contains(states, target) {
    fmt.Println("present")
}

Avoid calling Contains and then Index for the same target when you need the position. That can scan the slice twice. Call Index once and compare its result with -1.

slices.Index uses exact equality

The element type must be comparable because slices.Index searches using Go equality. That makes it a natural choice for values such as strings, integers, booleans, pointers, channels, and structs whose fields are all comparable.

For example, a small struct can be searched directly:

type Key struct {
    Region string
    ID     int
}

keys := []Key{
    {Region: "apac", ID: 10},
    {Region: "eu", ID: 20},
}

target := Key{Region: "eu", ID: 20}
fmt.Println(slices.Index(keys, target)) // 1

Every field participates in struct equality. A target with the same ID but a different Region doesn’t match.

Some values aren’t comparable. A struct containing a slice, map, or function can’t be passed to slices.Index as its element type. Matching may also require domain logic rather than exact equality. In those cases, use slices.IndexFunc:

type Job struct {
    ID   int
    Tags []string
}

jobs := []Job{
    {ID: 10, Tags: []string{"batch"}},
    {ID: 20, Tags: []string{"urgent"}},
}

index := slices.IndexFunc(jobs, func(job Job) bool {
    return job.ID == 20
})

fmt.Println(index) // 1

The distinction is useful at the call site: Index says “find this exact value,” while IndexFunc says “find the first value satisfying this rule.”

Empty and nil slices need no special guard

Unlike helpers that require at least one element, slices.Index handles empty input naturally:

var nilValues []string
emptyValues := []string{}

fmt.Println(slices.Index(nilValues, "x"))   // -1
fmt.Println(slices.Index(emptyValues, "x")) // -1

There is nothing to search, so both calls report absence. You don’t need a separate length check unless the application treats an empty collection as a distinct condition.

This makes Index convenient in parsing and filtering pipelines where an earlier stage may legitimately produce no values.

Use a loop when you need more than the first position

A direct loop can be clearer when the search must collect several results or perform extra work along the way.

Suppose you need every position containing "retry":

states := []string{"retry", "done", "retry", "failed"}

var positions []int
for i, state := range states {
    if state == "retry" {
        positions = append(positions, i)
    }
}

fmt.Println(positions) // [0 2]

Calling slices.Index repeatedly on progressively smaller subslices can produce the same result, but then the code must translate relative indexes back to positions in the original slice. A loop keeps that bookkeeping visible and straightforward.

The same applies when one pass needs to compute several facts. If you’re already counting values, validating order, or collecting errors, folding the equality check into that traversal can be simpler than adding a separate search.

Repeated lookups may call for a map

slices.Index performs a linear scan. For a short slice or an occasional lookup, that is often exactly the right trade-off: no extra data structure, no setup step, and the original order remains available.

The balance changes when code repeatedly searches a larger, mostly stable collection. An index map can move the lookup work to a preprocessing step:

names := []string{"alpha", "beta", "gamma"}

positions := make(map[string]int, len(names))
for i, name := range names {
    if _, exists := positions[name]; !exists {
        positions[name] = i
    }
}

index, found := positions["gamma"]
fmt.Println(index, found) // 2 true

The exists check preserves the first position when duplicate values occur. Without it, later duplicates overwrite earlier indexes, which doesn’t match slices.Index semantics.

A map also changes the maintenance cost. If the slice is mutated, inserted into, or reordered, the position map can become stale. For data that changes frequently or is searched only a few times, the direct slice scan is usually easier to keep correct.

Don’t sort just to search with slices.Index

slices.Index doesn’t require sorted input:

values := []int{40, 10, 30, 20}

fmt.Println(slices.Index(values, 30)) // 2

Sorting first would change the indexes, which defeats the point if you need the position in the original sequence.

For already sorted data that receives many searches, slices.BinarySearch can be a better operation. Its contract depends on sorted input and it returns both a position and a found flag. That is a different setup from Index, which works on arbitrary order and reports the first exact occurrence.

Choose based on the data you already have. Don’t add sorting solely to make a one-off lookup appear more sophisticated.

Keep the search operation aligned with the result you need

Use slices.Index when the element type is comparable, equality is exact, and the first matching position is the result the caller needs. Check for -1 before indexing, especially when the target can come from external input.

If the requirement shifts, change the operation with it: slices.Contains for existence, slices.IndexFunc for predicate-based matching, a loop for multiple positions, or a map for repeated keyed lookups. That keeps the code focused on the actual search instead of building extra machinery around a simple slice.