Two slices can represent ordered records rather than plain strings or numbers. Maybe they’re deployment steps, semantic-version parts, or structs sorted by a domain-specific key. When you need to answer which sequence comes first, slices.Compare isn’t enough if the elements don’t have Go’s built-in ordering.
slices.CompareFunc handles that case. It compares two slices from left to right and lets you define what it means for one pair of elements to be less than, equal to, or greater than the other.
How slices.CompareFunc decides the result
The function accepts two slice types and a comparator:
func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](
s1 S1,
s2 S2,
cmp func(E1, E2) int,
) intComparison starts at index 0. As long as the comparator returns zero, CompareFunc advances to the next pair. The first non-zero comparator result becomes the result of the whole call.
If every paired element compares equal, length breaks the tie: the shorter slice compares less than the longer one. Equal lengths produce zero.
That gives CompareFunc lexicographic behavior, much like comparing words character by character.
Compare slices of structs by domain fields
Suppose build records should be ordered first by service name and then by duration:
package main
import (
"cmp"
"fmt"
"slices"
)
type Build struct {
Service string
Duration int
}
func compareBuild(a, b Build) int {
if n := cmp.Compare(a.Service, b.Service); n != 0 {
return n
}
return cmp.Compare(a.Duration, b.Duration)
}
func main() {
baseline := []Build{
{Service: "api", Duration: 42},
{Service: "worker", Duration: 71},
}
candidate := []Build{
{Service: "api", Duration: 42},
{Service: "worker", Duration: 80},
}
fmt.Println(slices.CompareFunc(baseline, candidate, compareBuild)) // -1
}The first builds compare equal. At index 1, 71 comes before 80, so the result is negative. Nothing after that position would matter.
This is different from asking whether two slices contain the same values. CompareFunc describes ordering. If you only need equality under a custom rule, slices.EqualFunc communicates that intent more directly.
Prefixes are ordered by length
A common edge case appears when one slice is a complete prefix of the other:
short := []Build{
{Service: "api", Duration: 42},
}
long := []Build{
{Service: "api", Duration: 42},
{Service: "worker", Duration: 71},
}
result := slices.CompareFunc(short, long, compareBuild)
fmt.Println(result) // -1The comparator never finds a difference in the shared portion. Because short ends first, it compares less than long.
This detail matters for hierarchical data. A path such as ["api"] sorts before ["api", "v2"] when the shared elements compare equally. You don’t need special prefix handling around CompareFunc.
Nil and empty slices follow the same length rule. A nil slice and a non-nil empty slice both have length zero, so they compare equal when no elements are available to distinguish them.
The two slices can have different element types
CompareFunc doesn’t require both slices to contain the same type. Its generic signature has separate E1 and E2 parameters, which is useful when comparing a domain representation with a simpler external representation.
For example, a stored build can be compared with expected durations:
builds := []Build{
{Service: "api", Duration: 42},
{Service: "worker", Duration: 71},
}
expected := []int{42, 75}
result := slices.CompareFunc(builds, expected, func(build Build, duration int) int {
return cmp.Compare(build.Duration, duration)
})
fmt.Println(result) // -1The first pair is equal and the second pair differs, so the comparison stops there. This can avoid building a temporary slice merely to make both inputs share a type.
There’s a trade-off: the comparator defines only the relationship needed between E1 and E2. If the conversion or normalization rule is complicated, a named comparator is easier to test and review than a dense inline function.
A comparator must describe a consistent ordering
The comparator’s sign carries meaning. Return a negative value when the left element comes before the right, zero when they compare equal for this ordering, and a positive value when the left comes after the right.
For integer fields, avoid subtraction as a shortcut:
return a.Duration - b.DurationThat expression can overflow for values near the integer limits and produce a sign that doesn’t match the intended ordering. cmp.Compare avoids that problem:
return cmp.Compare(a.Duration, b.Duration)Another mistake is returning zero too early. Consider records that should be ordered by service and duration:
func compareBuild(a, b Build) int {
return cmp.Compare(a.Service, b.Service)
}With that comparator, two builds for the same service are intentionally equivalent even when their durations differ. CompareFunc will continue to the next slice position instead of noticing the duration difference. That’s correct only if duration genuinely doesn’t participate in the ordering.
Keep the comparator deterministic as well. A comparison that depends on changing external state can make the meaning of the result unstable and difficult to reason about.
CompareFunc stops at the first difference
Because later pairs can’t change a lexicographic result, CompareFunc doesn’t need to compare the rest of the slices after a non-zero result appears.
That behavior is useful when comparison work is non-trivial, but it also means the comparator shouldn’t be used for side effects. Code that expects every element pair to be visited will behave differently depending on where the first difference occurs.
If you need to validate every pair, write that validation as a separate pass. Comparison should answer the ordering question and stop once the answer is known.
Use the result by sign, not by an assumed magnitude
With slices.Compare, the result is normalized to -1, 0, or 1. CompareFunc is slightly different: when the comparator finds the first unequal pair, its non-zero result is returned directly.
A custom comparator could legally return -10 or 25. Callers should normally test the sign:
switch result := slices.CompareFunc(a, b, compareBuild); {
case result < 0:
fmt.Println("a comes first")
case result > 0:
fmt.Println("b comes first")
default:
fmt.Println("same ordering position")
}Don’t write result == -1 unless your comparator contract guarantees that exact value. Testing < 0, == 0, and > 0 works with any valid comparator.
Choose CompareFunc when ordering is the real question
Use slices.CompareFunc when two sequences need lexicographic ordering and their elements require a custom comparison rule. It works especially well for structs and for comparisons between different element types.
Keep the comparator small enough that its ordering is obvious, include every field that should affect equality within that ordering, and treat the returned value by its sign. If the code only needs a yes-or-no equality check, use slices.EqualFunc instead; if it needs to reorder one slice, use the sorting helpers. Picking the operation that matches the question keeps the comparison rule easier to understand later.