Two slices can contain the same number of elements and still represent different sequences. When equality means “same length, same value at every index,” slices.Equal expresses that check directly without a manual loop.
The function is small, but a few details matter in production code. Element order counts, nil and empty slices compare equal, element types must be comparable, and floating-point NaN values don’t compare equal to themselves.
Compare Go slices with slices.Equal
For strings, integers, booleans, and other comparable element types, pass both slices directly:
package main
import (
"fmt"
"slices"
)
func main() {
expected := []string{"queued", "running", "done"}
actual := []string{"queued", "running", "done"}
fmt.Println(slices.Equal(actual, expected))
}The output is:
trueslices.Equal first requires matching lengths. It then compares elements in increasing index order and stops at the first unequal pair. It doesn’t reorder or modify either input.
Changing one position makes the result false:
left := []int{10, 20, 30}
right := []int{10, 30, 20}
fmt.Println(slices.Equal(left, right)) // falseBoth slices contain the same values, but they’re different sequences. That distinction is useful for ordered configuration, command arguments, protocol fields, and any other data where position carries meaning.
Order is part of slice equality
slices.Equal isn’t a set comparison. These slices don’t compare equal:
a := []string{"read", "write"}
b := []string{"write", "read"}
fmt.Println(slices.Equal(a, b)) // falseIf order has no meaning in your domain, decide how duplicates should be treated before choosing another approach. Sorting copies and comparing them can work when elements are ordered and duplicate counts matter:
a := []int{3, 1, 2, 2}
b := []int{2, 3, 2, 1}
sortedA := slices.Clone(a)
sortedB := slices.Clone(b)
slices.Sort(sortedA)
slices.Sort(sortedB)
fmt.Println(slices.Equal(sortedA, sortedB)) // trueCloning matters here because slices.Sort changes its argument in place. Sorting the original slices just to compare them can introduce an unrelated mutation that is easy to miss during review.
A map-based frequency count is another option when sorting isn’t suitable. The right representation depends on whether order, duplicate counts, and original storage all matter to the caller.
Nil and empty slices compare equal
Go code often distinguishes a nil slice from an allocated empty slice when serialization or API shape matters. slices.Equal deliberately doesn’t make that distinction:
var nilValues []string
emptyValues := []string{}
fmt.Println(nilValues == nil) // true
fmt.Println(emptyValues == nil) // false
fmt.Println(slices.Equal(nilValues, emptyValues)) // trueBoth slices have length zero, so they compare equal under slices.Equal.
If nilness is part of the application contract, check it separately:
sameShape := (left == nil) == (right == nil)
sameValues := slices.Equal(left, right)
if sameShape && sameValues {
fmt.Println("same representation")
}This comes up in tests around JSON payload preparation, optional fields, and APIs that use nil to represent an absent collection. A value-level equality helper can’t preserve a distinction its contract intentionally ignores.
Element types must be comparable
The element type passed to slices.Equal must satisfy Go’s comparable constraint. A slice of strings works, and so does a slice of structs whose fields are all comparable:
type Key struct {
Region string
ID int
}
left := []Key{{Region: "apac", ID: 10}}
right := []Key{{Region: "apac", ID: 10}}
fmt.Println(slices.Equal(left, right)) // trueA struct containing a slice, map, or function isn’t comparable, so a slice of that struct can’t be passed to slices.Equal.
For those values, or for domain-specific equality, use slices.EqualFunc. The callback defines what one pair of elements means to compare equal:
type Job struct {
ID int
Tags []string
}
left := []Job{{ID: 7, Tags: []string{"batch"}}}
right := []Job{{ID: 7, Tags: []string{"urgent"}}}
sameIDs := slices.EqualFunc(left, right, func(a, b Job) bool {
return a.ID == b.ID
})
fmt.Println(sameIDs) // trueThat code intentionally ignores Tags. Keeping the comparison rule next to the call makes that choice visible instead of hiding it inside an unrelated helper.
Floating-point NaN needs explicit handling
Floating-point values are comparable in Go, so []float64 can be passed to slices.Equal. NaN is the notable edge case: a NaN value is not equal to itself.
values := []float64{1, math.NaN(), 3}
copyOfValues := slices.Clone(values)
fmt.Println(slices.Equal(values, copyOfValues)) // falseIf the application treats two NaN values as equivalent, define that rule with slices.EqualFunc:
same := slices.EqualFunc(values, copyOfValues, func(a, b float64) bool {
return a == b || (math.IsNaN(a) && math.IsNaN(b))
})
fmt.Println(same) // trueApproximate floating-point comparison is a separate requirement. If values should match within a tolerance, encode that tolerance in the callback rather than expecting exact equality to account for rounding differences.
Avoid manual loops that miss length checks
A handwritten comparison loop can be correct, but it creates more places for small mistakes. This version is incomplete:
same := true
for i := range left {
if left[i] != right[i] {
same = false
break
}
}If right is shorter, it can panic. If right is longer but shares the same prefix, the loop can report a result that doesn’t represent full slice equality unless a length check is added first.
slices.Equal(left, right) carries both requirements in one operation. An explicit loop still makes sense when the comparison must also report the first mismatch, collect diagnostics, or perform other work during the same traversal.
For example, tests sometimes need the exact position of a difference rather than a boolean. In that case, a loop can return richer information:
func firstMismatch(a, b []string) int {
limit := min(len(a), len(b))
for i := 0; i < limit; i++ {
if a[i] != b[i] {
return i
}
}
if len(a) != len(b) {
return limit
}
return -1
}The extra code earns its place because the caller needs more than equality.
Choose equality that matches the data contract
Use slices.Equal when two slices should have the same length and equal comparable elements at the same indexes. It handles empty input without special guards and leaves both slices unchanged.
Before using it as a general collection comparison, check the contract around order, nilness, custom fields, and floating-point values. Move to slices.EqualFunc when equality needs a domain rule, and use a different representation when order shouldn’t count at all. Matching the operation to the data contract keeps a short comparison from carrying hidden assumptions.