Two Go maps can represent the same logical data even when their value types differ or their values need domain-specific comparison. maps.EqualFunc handles that case by matching keys normally while delegating value comparison to a caller-supplied function.

That split matters. The comparator controls value equivalence only. It cannot redefine key identity, compensate for a missing key, or make maps with different entry counts equal.

Key membership is checked before value equivalence

The function accepts two maps with the same key type but potentially different value types:

func EqualFunc[
    M1 ~map[K]V1,
    M2 ~map[K]V2,
    K comparable,
    V1, V2 any,
](m1 M1, m2 M2, eq func(V1, V2) bool) bool

Keys retain the built-in map requirement of being comparable. EqualFunc uses ordinary key equality for matching entries. The value types have no comparable constraint because the supplied function performs that part of the comparison.

A direct use case is comparing text stored in different representations:

package main

import (
    "fmt"
    "maps"
    "strings"
)

func main() {
    left := map[int]string{
        10: "READY",
        20: "closed",
    }

    right := map[int][]byte{
        10: []byte("ready"),
        20: []byte("CLOSED"),
    }

    same := maps.EqualFunc(left, right, func(a string, b []byte) bool {
        return strings.EqualFold(a, string(b))
    })

    fmt.Println(same)
}

The two value types are different, yet each key can still be paired because both maps use int keys. The comparator receives values only for corresponding keys.

The comparator defines the equality policy

maps.Equal uses == for values, which restricts it to comparable value types. maps.EqualFunc removes that restriction and makes the equality rule explicit.

This is useful for values such as slices:

package main

import (
    "bytes"
    "fmt"
    "maps"
)

func main() {
    a := map[string][]byte{
        "etag": {0x61, 0x62, 0x63},
    }

    b := map[string][]byte{
        "etag": {0x61, 0x62, 0x63},
    }

    same := maps.EqualFunc(a, b, bytes.Equal)
    fmt.Println(same)
}

A slice cannot be compared with == except against nil, so maps.Equal cannot serve this shape. bytes.Equal supplies the missing value relation without requiring a wrapper type or a manual map loop.

The same mechanism can encode a narrower notion of equality for structs. A comparator might inspect an identifier and status while intentionally ignoring a timestamp. In that case, EqualFunc reports equality according to that selected projection, not according to full struct identity.

That behavior should be visible at the call site. A comparator that silently ignores fields can otherwise make a strict-looking equality check less strict than its name suggests.

Different map sizes cannot compare equal

Value comparison does not override map structure. If the maps contain different numbers of entries, the result is false.

Likewise, equal lengths are not sufficient. Every key in one map must have a corresponding key in the other map, and the comparator must accept the associated values. A comparator that always returns true still cannot make distinct key sets equal.

left := map[string]int{"a": 1}
right := map[string]int{"b": 1}

same := maps.EqualFunc(left, right, func(_, _ int) bool {
    return true
})

same is false because "a" and "b" do not form the same key set.

This boundary keeps key semantics predictable. Applications that need case-insensitive string keys, normalized paths, or another custom key relation need to normalize or transform keys before comparison rather than placing that policy in the value comparator.

Nil and empty maps have the same entry set

A nil map has no entries, as does an allocated empty map. For equality based on contained key-value pairs, that distinction does not create a mismatch.

var left map[string][]byte
right := map[string][]byte{}

same := maps.EqualFunc(left, right, bytes.Equal)

The result is true. No value pairs need comparison.

Code that treats nil as a distinct state must preserve that check separately. EqualFunc answers whether the maps contain equivalent entries under the supplied value relation; it is not a test of allocation state.

Comparator behavior belongs to the caller

The standard library does not impose mathematical properties on the supplied comparator. A function can be asymmetric, stateful, time-dependent, or otherwise unsuitable as an equality relation, and EqualFunc cannot repair those semantics.

For stable results, the comparator should normally behave as an equality relation for the values being compared. It should also avoid mutating either map during comparison. Keeping the function free of side effects makes the result depend on the map contents rather than on traversal details.

Floating-point comparison deserves particular care. A tolerance-based comparator can be appropriate when the application has a defined tolerance model, but a fixed absolute epsilon is not universally suitable across value scales. The comparison policy should come from the data’s numerical contract rather than from EqualFunc itself.

Map keys also retain Go’s normal behavior. The maps package does not add special handling for non-reflexive keys such as floating-point NaN values. A value comparator cannot change that key-level behavior.

Custom comparison is most useful at representation boundaries

maps.EqualFunc is a compact fit when two maps share key identity but differ in value representation or value equivalence. It can compare bytes with strings, slices with slices, or structs under an explicitly selected field policy while leaving map membership semantics intact.

It is less suitable when comparison requires transforming keys, pairing entries by something other than key identity, or producing detailed mismatch information. Those cases usually need an explicit comparison routine that can model the extra structure and report it.

The useful boundary is therefore narrow and clear: ordinary Go map membership on the outside, caller-defined value equivalence on the inside. When that matches the data model, maps.EqualFunc keeps the comparison policy local without hiding the rules that determine the result.