Two slices can represent the same information without being directly comparable element by element. One side might contain structs while the other contains strings. Text may need case-insensitive comparison. IDs may arrive in different concrete types even though your application treats them as the same value.
When equality depends on a rule rather than Go’s == operator, slices.EqualFunc puts that rule in one place and handles the slice traversal for you.
What slices.EqualFunc actually compares
The function is defined for two slice types and accepts an equality function for their elements:
func EqualFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](
s1 S1,
s2 S2,
eq func(E1, E2) bool,
) boolIt first checks the lengths. Different lengths mean the slices aren’t equal, so the equality function isn’t called at all. If the lengths match, elements are compared from index 0 upward. Comparison stops as soon as eq returns false.
That gives slices.EqualFunc two useful properties: your equality rule only deals with a pair of elements, and it can compare slices whose element types differ.
Here’s a small example where application users are compared with email addresses returned by another system:
package main
import (
"fmt"
"slices"
"strings"
)
type User struct {
Email string
}
func main() {
users := []User{
{Email: "Ada@Example.com"},
{Email: "bob@example.com"},
}
emails := []string{
"ada@example.com",
"BOB@example.com",
}
same := slices.EqualFunc(users, emails, func(user User, email string) bool {
return strings.EqualFold(user.Email, email)
})
fmt.Println(same) // true
}A plain slices.Equal can’t express this comparison. Its elements must be comparable and both slices use the same element type. EqualFunc moves the definition of equality into the callback instead.
Put domain equality in the callback
The callback should answer one narrow question: does the element from the first slice represent the same value as the element at the same position in the second slice?
Suppose an API response uses a transport type while the rest of the program uses a domain type:
type Product struct {
SKU string
Price int64
}
type ProductResponse struct {
Code string
PriceCents int64
}
same := slices.EqualFunc(products, response, func(p Product, r ProductResponse) bool {
return p.SKU == r.Code && p.Price == r.PriceCents
})This is often clearer than converting the entire response into []Product solely to compare it once. The equality function documents which fields matter for this particular check.
That last point matters. Struct equality and domain equality aren’t always the same thing. A cached timestamp, display label, or internal database ID may be irrelevant when deciding whether two business records represent the same value. EqualFunc lets the comparison say that explicitly.
Normalize carefully instead of hiding mismatches
Custom equality is useful for normalized data, but normalization rules should match the domain rather than being added for convenience.
Case-insensitive email comparison is a simple illustration:
same := slices.EqualFunc(current, expected, strings.EqualFold)For other data, you may need a more specific rule:
same := slices.EqualFunc(pathsA, pathsB, func(a, b string) bool {
return strings.TrimSuffix(a, "/") == strings.TrimSuffix(b, "/")
})That code intentionally treats "/docs" and "/docs/" as equal. It’s only correct if your application has already decided that a trailing slash carries no meaning in this context.
Avoid callbacks that normalize so aggressively that distinct values collapse together. Lowercasing identifiers, discarding punctuation, or parsing strings leniently can turn malformed input into an apparent match. If parsing can fail, handle that failure deliberately:
same := slices.EqualFunc(numbers, encoded, func(n int, text string) bool {
parsed, err := strconv.Atoi(text)
if err != nil {
return false
}
return n == parsed
})Returning false on invalid input is appropriate when invalid data simply means “not equal.” If malformed input is an error your caller needs to distinguish, slices.EqualFunc may be the wrong abstraction because its callback can’t return an error.
Length and order are part of equality
slices.EqualFunc compares sequences, not sets. Both length and position matter.
These slices are not equal even if the callback ignores letter case:
left := []string{"api", "worker"}
right := []string{"worker", "API"}The first elements are compared with each other, then the second elements. EqualFunc doesn’t search for a matching element elsewhere in the other slice.
Likewise, a two-element slice never equals a one-element slice. The function rejects the length mismatch before invoking your callback. This can be useful when the callback performs parsing or other nontrivial work because no unnecessary comparisons happen when the sequence sizes already disagree.
If order shouldn’t matter, define that requirement separately. Depending on the data, you might sort copies before comparing them, count occurrences in a map, or build a set-like representation. Using an equality callback that somehow searches the other slice tends to obscure the sequence semantics and can make the comparison much more expensive.
Empty and nil slices compare equal
A nil slice and an empty non-nil slice both have length zero. Since there are no element pairs to reject, slices.EqualFunc considers them equal:
var a []int
b := []string{}
same := slices.EqualFunc(a, b, func(int, string) bool {
return false
})
fmt.Println(same) // trueThe callback isn’t called in this example.
That behavior is usually convenient when you’re comparing contents. It isn’t suitable when nil has a separate meaning, such as “not provided” while an empty slice means “provided, with no values.” Check nilness before calling EqualFunc if your protocol or application preserves that distinction.
Short-circuiting affects callbacks with side effects
The equality function is called only until the result is known. That means it shouldn’t be used as a place to perform work that must happen for every element.
Consider this code:
comparisons := 0
same := slices.EqualFunc(a, b, func(x, y string) bool {
comparisons++
return x == y
})If the first pair differs, comparisons is 1 even when both slices contain thousands of elements. If the lengths differ, it remains 0.
Counting calls for diagnostics is harmless when you understand that behavior, but relying on the callback to update every record, emit every metric, or validate every element is a mistake. Equality checks are allowed to stop early. Keep required side effects in a separate pass.
EqualFunc is not a comparator
Go’s slice helpers use two related callback shapes that are easy to mix up. slices.EqualFunc expects a boolean equality function:
func(a, b T) boolSorting and lexicographic comparison helpers such as slices.SortFunc and slices.CompareFunc use a three-way comparator instead:
func(a, b T) intA comparator describes ordering; an equality function only describes whether a pair matches. Don’t force ordering logic into EqualFunc when equality is all you need.
The reverse is also worth watching. If you’ve already written a comparator that returns zero for values your domain considers equal, wrapping it can be reasonable:
same := slices.EqualFunc(a, b, func(x, y Item) bool {
return compareItem(x, y) == 0
})Do this only when the comparator’s zero relation truly matches the equality you want. A sort comparator may intentionally ignore fields that should matter elsewhere.
When a handwritten loop is still better
EqualFunc is a good fit when the result is simply equal or not equal. A manual loop is clearer when the caller needs richer information.
For example, configuration validation may need to report the first mismatching index and both values. EqualFunc discards that context:
if len(want) != len(got) {
return fmt.Errorf("length mismatch: want %d, got %d", len(want), len(got))
}
for i := range want {
if !sameConfig(want[i], got[i]) {
return fmt.Errorf("config mismatch at index %d", i)
}
}The loop is longer, but it matches the real requirement better. The same applies when comparison can return an error or when you need to collect every mismatch rather than stop at the first one.
Use slices.EqualFunc when equality has a nameable rule
Reach for slices.EqualFunc when two ordered sequences should have the same length and each corresponding pair can be judged by a clear equality rule. It’s especially handy for comparing different element types or applying domain-specific normalization without allocating converted copies.
Keep the callback small and unsurprising. If nilness matters, errors need to escape, order is irrelevant, or callers need mismatch details, handle those requirements explicitly rather than stretching a boolean equality helper beyond what it represents.