Two slices can represent the same ordered data even when their element types differ. One side might contain integers while another contains numeric strings, or two struct types might expose the same key through different fields. Plain slices.Compare can’t express those cases because it relies on the element type’s built-in ordering.

slices.CompareFunc lets you supply the element comparison. It still compares the slices lexicographically, so the first unequal pair decides the result; the custom function only defines how each pair is ordered.

How slices.CompareFunc decides the result

The function accepts two slices and a comparator for one element from each side:

func CompareFunc[S1 ~[]E1, S2 ~[]E2, E1, E2 any](
    s1 S1,
    s2 S2,
    cmp func(E1, E2) int,
) int

A negative result means the first slice sorts before the second. Zero means the slices compare equal. A positive result means the first slice sorts after the second.

Consider two slices with different integer types:

package main

import (
    "cmp"
    "fmt"
    "slices"
)

func main() {
    left := []int{10, 20, 30}
    right := []int64{10, 20, 40}

    result := slices.CompareFunc(left, right, func(a int, b int64) int {
        return cmp.Compare(int64(a), b)
    })

    fmt.Println(result)
}

The output is:

-1

The first two pairs compare equal. At the third pair, 30 comes before 40, so the function returns a negative value without needing any later comparison.

The first unequal pair controls lexicographic order

Lexicographic comparison works like dictionary ordering. Elements are compared from index zero upward until one pair differs.

left := []int{2, 100, 999}
right := []int{3, 0, 0}

result := slices.CompareFunc(left, right, func(a, b int) int {
    return cmp.Compare(a, b)
})

fmt.Println(result) // -1

The values 100 and 999 never affect the ordering. The first pair is 2 and 3, and that already establishes that left comes first.

This matters when a comparator does nontrivial work. Don’t assume every pair will be visited, and don’t put required side effects inside the comparison function.

Length matters only after the shared prefix matches

If every compared pair is equal, slice length breaks the tie.

left := []string{"api", "v1"}
right := []string{"api", "v1", "users"}

result := slices.CompareFunc(left, right, func(a, b string) int {
    return cmp.Compare(a, b)
})

fmt.Println(result) // -1

The shorter slice sorts first because it is a prefix of the longer one.

Two empty slices compare equal. A nil slice and an empty non-nil slice also compare equal because there are no differing elements and their lengths are both zero.

That behavior is useful when comparison is about sequence contents rather than allocation state. If nil and empty carry different domain meanings, CompareFunc alone can’t represent that distinction; check nilness separately.

Compare different element types directly

The two element types are independent type parameters. This avoids temporary conversion slices when the values can be compared pair by pair.

For example, one source may provide numeric IDs as integers while another provides decimal strings:

package main

import (
    "cmp"
    "fmt"
    "slices"
    "strconv"
)

func main() {
    numbers := []int{7, 20, 42}
    text := []string{"7", "20", "50"}

    result := slices.CompareFunc(numbers, text, func(n int, s string) int {
        parsed, err := strconv.Atoi(s)
        if err != nil {
            return 1
        }
        return cmp.Compare(n, parsed)
    })

    fmt.Println(result) // -1
}

At index two, 42 sorts before parsed value 50.

There is a design choice hidden in the parse-error branch. Returning 1 assigns malformed text an ordering relative to the integer, but that policy may not match your application. If malformed input is an error rather than a sortable value, validate or parse it before calling CompareFunc instead of smuggling error handling into the comparator.

Compare structs by the field that defines order

Structs aren’t ordered types, but a field inside them often is. A comparator can project each element onto the relevant key.

type LocalUser struct {
    ID   int
    Name string
}

type RemoteUser struct {
    UserID int64
    Label  string
}

local := []LocalUser{
    {ID: 10, Name: "Ari"},
    {ID: 20, Name: "Bima"},
}

remote := []RemoteUser{
    {UserID: 10, Label: "A"},
    {UserID: 25, Label: "B"},
}

result := slices.CompareFunc(local, remote, func(a LocalUser, b RemoteUser) int {
    return cmp.Compare(int64(a.ID), b.UserID)
})

fmt.Println(result) // -1

Only the ID fields participate. Names and labels are deliberately ignored.

That can be exactly right for ordering by identity, but it also means two records with equal IDs compare equal even if every other field differs. The comparator defines equality for this operation, not full structural equality.

Case-insensitive comparison needs a consistent rule

Custom comparison is useful when built-in string ordering doesn’t match the desired sequence order.

left := []string{"Alpha", "beta"}
right := []string{"alpha", "BETA"}

result := slices.CompareFunc(left, right, func(a, b string) int {
    return strings.Compare(strings.ToLower(a), strings.ToLower(b))
})

fmt.Println(result) // 0

For this comparator, letter case doesn’t distinguish values.

If normalization allocates or performs substantial work, doing it inside every comparison may be wasteful in a hot path. Precomputing normalized keys can be cleaner when the same data is compared repeatedly.

Also keep equality semantics in mind. A zero result from CompareFunc means every corresponding pair compared as zero and the lengths matched. It does not mean the original values are byte-for-byte or field-for-field identical.

Comparator sign matters more than exact magnitude

A comparator’s contract is about sign. Negative means before, zero means equal, and positive means after. Callers shouldn’t depend on receiving exactly -1 or 1.

Using cmp.Compare is a straightforward option for ordered values:

func(a, b int) int {
    return cmp.Compare(a, b)
}

Avoid subtraction as a shortcut:

func(a, b int) int {
    return a - b
}

Subtraction can overflow for sufficiently distant integer values, which can produce a sign that no longer represents the intended order. cmp.Compare expresses the operation directly without that arithmetic hazard.

Don’t use comparison as an equality test without checking the semantics

It can be tempting to write:

same := slices.CompareFunc(a, b, comparator) == 0

That is valid only when the comparator’s notion of zero is the equality you actually want.

A case-insensitive comparator considers "API" and "api" equal. A struct comparator that checks only an ID ignores changes in other fields. Those semantics are useful for ordering, but they may be too broad for validation or change detection.

When you need custom pairwise equality rather than ordering, slices.EqualFunc states that intent more clearly.

Choose CompareFunc when ordering is the real operation

Use slices.CompareFunc when you need lexicographic ordering and the element comparison isn’t covered by ordinary ordered values. It is a good fit for cross-type sequences, structs compared by keys, normalized strings, and domain-specific sort rules.

If both slices contain the same ordered element type and built-in ordering is correct, slices.Compare is shorter. If you only need equal-or-not-equal semantics, slices.Equal or slices.EqualFunc is usually a clearer match.

Keep the comparator focused on a stable ordering rule, remember that comparison stops at the first unequal pair, and treat zero according to the comparator you supplied rather than assuming it means full structural identity.