Why Go’s New Hasher[T] Interface Redefines Safe Generic Hashing
Go’s 2025 addition of the generic Hasher[T] interface to hash/maphash standardizes safe hashing by embedding a random seed throughout recursive calls, resolves previous design flaws, and provides clear guidelines for custom hashers, comparable types, and practical applications such as Bloom filters, all while ensuring performance and correctness.
Background
In the Go 2025 release the hash/maphash package gained a new generic interface Hasher[T]. The change was driven by go/types issue #69420, which required a unified way to hash values and test equality for the type system’s internal hash tables. Earlier proposals used separate function types ( HashFunc[T] func(T) uint64 and EqFunc[T] func(x, y T) bool) but omitted a secure, seed‑aware convention.
Why a seed is mandatory
Hash‑flooding attacks exploit deterministic hash functions by forcing many keys to collide, degrading hash‑table look‑ups to O(n). The standard defence is to generate a random maphash.Seed for each hash‑table instance and incorporate it into every hashing step. maphash.Hash already performs this, so any safe hashing API must accept a seed.
Design evolution
Early designs bundled Hash and Equal in a single interface but struggled with propagating the seed through recursive calls. Four concrete options were discussed:
Pass Seed as a parameter and return uint64.
Store the seed inside the hasher and expose it via a generic type parameter.
Let the hasher hold a seed internally and provide a Reset(Seed) method.
Use a factory NewHasher(Seed) Hasher.
The decisive question was: How should the seed be propagated during recursive hashing?
Key turning point – pass *maphash.Hash instead of Seed
Initially each hash function received a maphash.Seed and returned a uint64. This required creating a new maphash.Hash at every recursion level and manually merging intermediate results:
type H1 struct{}
func (H1) Hash(seed maphash.Seed, v T1) uint64 {
var mh maphash.Hash
mh.SetSeed(seed)
mh.WriteString(v.a)
// hash sub‑field, then combine
maphash.WriteComparable(&mh, H2{}.Hash(seed, v.b))
return mh.Sum64()
}In December 2024 the design shifted to passing a pointer to a shared *maphash.Hash object down the call chain. All nested calls write into the same hash state, guaranteeing that the seed participates from the first step and is consistently used throughout recursion.
type Hasher[T any] interface {
Hash(*maphash.Hash, T)
Equal(T, T) bool
}
type H1 struct{}
func (H1) Hash(mh *maphash.Hash, v T1) {
mh.WriteString(v.a)
H2{}.Hash(mh, v.b) // continue with the same hash
}Why not attach methods directly to the type
Binding Hash() and Equal() to the target type (as in Java’s hashCode() / equals()) was rejected for three reasons:
The type may come from a third‑party library and cannot be modified.
Interfaces such as io.Reader cannot implement additional methods.
A single type may need multiple equality semantics (e.g., case‑sensitive vs. case‑insensitive strings).
Therefore the hasher must be an independent type, not a method set on T.
Zero‑value guarantees and default implementations
The final design requires a Hasher to be stateless and usable directly from its zero value. For comparable types the standard library provides ComparableHasher[T], which forwards equality to == and uses maphash.WriteComparable for hashing.
type ComparableHasher[T comparable] struct{}
func (ComparableHasher[T]) Hash(h *maphash.Hash, v T) {
maphash.WriteComparable(h, v)
}
func (ComparableHasher[T]) Equal(x, y T) bool { return x == y }Implementing a custom hasher
When defining a custom hasher, follow these steps:
Identify the fields that determine equality.
Implement Equal to compare exactly those fields.
Implement Hash to write the same fields, in the same order, into the supplied *maphash.Hash.
For unordered structures (e.g., sets) hash each element separately and combine the results with XOR, which is order‑independent:
func (setHasher[T]) Hash(hash *maphash.Hash, set *Set[T]) {
var accum uint64
for elem := range set.Elements() {
var sub maphash.Hash
sub.SetSeed(hash.Seed())
maphash.WriteComparable(&sub, elem)
accum ^= sub.Sum64()
}
maphash.WriteComparable(hash, accum)
}Real‑world example: Bloom filter
A Bloom filter implementation submitted in January 2026 demonstrates the API. The filter stores a Hasher[V], a slice of seeds (one per hash function), and a bitmap. Insertion and membership checks iterate over all seeds, using a shared *maphash.Hash to compute each hash.
type BloomFilter[V any] struct {
hasher Hasher[V]
seeds []maphash.Seed
bytes []byte
}
func (f *BloomFilter[V]) Insert(v V) {
for _, seed := range f.seeds {
idx, bit := f.locate(seed, v)
f.bytes[idx] |= bit
}
}
func (f *BloomFilter[V]) Contains(v V) bool {
for _, seed := range f.seeds {
idx, bit := f.locate(seed, v)
if f.bytes[idx]&bit == 0 {
return false // definite negative
}
}
return true // possible positive
}The helper locate creates a temporary maphash.Hash, sets the seed, invokes the hasher, and maps the 64‑bit result to a bitmap position using Lemire’s fast reduction.
func (f *BloomFilter[V]) locate(seed maphash.Seed, v V) (uint64, byte) {
var h maphash.Hash
h.SetSeed(seed)
f.hasher.Hash(&h, v)
hash := h.Sum64()
index := reduce(hash, uint64(len(f.bytes)))
mask := byte(1 << (hash % 8))
return index, mask
}Final accepted API (2025‑04‑02)
type Hasher[T any] interface {
// Hash appends the logical content of value to h
Hash(*maphash.Hash, T)
// Equal reports whether two values are equal
Equal(x, y T) bool
}
type ComparableHasher[T comparable] struct{}Key constraints:
If Equal(a,b) returns true, then calling Hash on a and b must write identical bytes to the same *maphash.Hash. Hash does not return a value; it mutates the supplied hash object.
Implementations must be stateless; the zero value is ready for use.
Design trade‑offs
Unmodifiable third‑party types vs. requiring types to implement hashing → Hasher is a separate type, not a method on T .
Seed used only at the end vs. seed throughout computation → Seed participates from the first step and is passed to every recursive call .
Pass Seed and return uint64 vs. pass *maphash.Hash → Pass *maphash.Hash for natural recursion .
Dynamic interface dispatch vs. static type parameters → Static type parameters are used for better performance .
Practical guidance for Go developers
For ordinary comparable types, use ComparableHasher[T] directly – zero configuration.
When custom equality rules are needed, implement both Hash and Equal consistently, ensuring they operate on the same fields and transformations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
