Why does a Go container declared with [K comparable] accept K = any, and how can that panic at run time?
answer
- comparable means == cannot panic
- implements and satisfies are two different verbs
- an interface is comparable but not strictly
- the panic surfaces inside your generic container
basics
~20 scomparable's type set is the strictly comparable types, but Go's satisfaction rule makes an exception for merely comparable type arguments, so an interface type such as any is accepted. Comparing or hashing an interface holding a slice then panics at run time.
solid answer
~50 s`comparable` denotes the **strictly comparable** types — those whose `==` is defined and cannot panic. Interface types are comparable but not strictly: comparing two interface values panics when the dynamic type is a slice, map or function. Go's constraint-satisfaction rule has a deliberate exception for exactly this case, so `Index[any, int]` compiles even though `any` does not *implement* `comparable`. The consequence lands at run time: storing a `[]string` in a `map[any]V` panics with an unhashable-key error, and a bare `==` on such values panics with an uncomparable-type error. The stack trace points into your generic code, not the caller's, which is why library authors get paged for it. If your package cannot tolerate that, narrow the constraint to a union of concrete terms like `~string | ~int64`, or document that interface keys are the caller's risk.
code
go · 11 linestype Index[K comparable, V any] struct {
byKey map[K]V
}
func (ix *Index[K, V]) Put(k K, v V) {
ix.byKey[k] = v
}
// Index[any, int] compiles: any is comparable, though not strictly.
// Put with a []string key then panics at run time on the map write.
// Index[[]byte, int] is a compile error: slices are not comparable at all.go deeper
Know that comparable means the type supports == and that slices, maps and functions do not.
Explain the difference between strictly comparable types and interface types whose == may panic depending on the dynamic value inside them.
Be able to read the panic, trace it back to the type argument at the instantiation site, and say concretely what you would change in the container's signature or at its input boundary.
Own the API call: whether your shared container advertises an open comparable key and pushes the run-time risk onto every consuming team, or narrows the type set and forces some of them to convert their keys.
## Two levels of comparable Go distinguishes two things that sound identical: - **Comparable**: `==` and `!=` are defined for the type. Interface types qualify — you can always write `a == b` for two `any` values and it compiles. - **Strictly comparable**: `==` is defined *and cannot panic*. Booleans, numbers, strings, pointers, channels, arrays of strictly comparable elements and structs of strictly comparable fields qualify. Interfaces do not, because comparing two interface values whose dynamic type is a slice, map or function panics. The predeclared constraint `comparable` denotes the **strictly comparable** types. That is why `comparable` is the right bound for a map key: it promises the operation exists. ## The satisfaction exception If membership were the only rule, `Index[any, int]` would be a compile error, and for the first generics releases it was — which made it impossible to write a generic container that a caller could instantiate with `any`, even though the equivalent plain `map[any]V` had always been legal. Go therefore added an exception to constraint *satisfaction*: a type argument satisfies a strictly comparable constraint if it is comparable at all, even when it does not implement the constraint. So two verbs now differ, and interviewers like the distinction: - `any` **implements** `comparable`? No — it is not in the type set. - `any` **satisfies** `comparable`? Yes — by the exception. Everything you can do with a `K comparable` type parameter is still permitted, including keying a map. The compiler simply stops guaranteeing that it will not panic. ## What the failure looks like A ranking service keeps a generic index: ```go type Index[K comparable, V any] struct{ byKey map[K]V } ``` A caller who does not know the key type ahead of time instantiates `Index[any, int]` and, somewhere down a decoding path, stores a key that is a `[]string` — perhaps a JSON value that decoded into a slice rather than a string. The write panics with a run-time error about hashing an unhashable type; a plain `==` on the same values would panic about comparing an uncomparable type. The diagnostic detail that matters on call: the panic's stack trace runs through the generic method in *your* package, one frame below the caller's decode loop. It reads like a bug in the container. The actual defect is the pair of the instantiation (`any`) and the value that reached it, and you find it by looking at the type argument at the instantiation site and at what the dynamic type of the offending key was. ## What a library author can do about it There is no clean way to reject the interface instantiation at compile time — an interface that embeds `comparable` is still a strictly comparable constraint, so the same exception applies to it. The options are design choices, not tricks: 1. **Narrow the constraint.** `~string | ~int64 | ~int` admits no interface type at all, so the panic becomes impossible. The cost is that callers with struct keys or genuinely dynamic keys cannot use your container. 2. **Take the key as a concrete type.** A ranking index keyed by `string` needs no type parameter for K and no discussion. 3. **Accept it and document it.** State in the doc comment that instantiating with an interface type means the caller owns the comparability of the dynamic values, exactly as they would with a plain map. 4. **Normalise at the boundary.** Convert incoming keys to a canonical string or a fixed-size array before they reach the map, so no interface value is ever hashed. ## Related compile-time rejections Not everything moves to run time. A type argument that is *not comparable at all* is still rejected by the compiler: `Index[[]byte, int]` fails, and so does a struct type with a slice field. Only interface types slip through, because they are comparable in the weak sense. Keeping that boundary straight is the whole answer: the constraint eliminates the never-comparable types at compile time and delegates the maybe-comparable ones to run time.
- Can you stop callers instantiating your container with an interface type?Not with `comparable`, and not by embedding it in your own constraint — the satisfaction exception applies to any strictly comparable constraint. Narrow the type set instead, to a union such as `~string | ~int64`, which contains no interface type, or take a concrete key type and drop the parameter.
- Which type arguments does comparable still reject at compile time?The types that are not comparable at all: slices, maps and functions, plus structs and arrays containing them. Those are outside the type set and no exception rescues them, so `Index[[]byte, V]` fails to build. Only interface types benefit from the relaxation.
- How do you find the cause from the panic alone?Read the message for the offending dynamic type — it names the uncomparable or unhashable type — then walk the stack out of the generic method to the frame that instantiated the container and see which type argument was supplied for the key. The pairing of an interface type argument with that dynamic type is the defect.
saying these in an interview costs you the question
- Says comparable guarantees no instantiation can panic
- Believes instantiating with any is a compile error in current Go
- Thinks the race detector or go vet would catch it
- Claims the constraint is re-checked at run time