Go Generics: GC Shape Stenciling & Dictionary Method Passing
- Last updated
- Prerequisites:
- Go type parameters and constraints
- Understanding of code generation and interface dispatch
- go
- generics
- type-parameters
- gc-shape
- stenciling
- dictionary
Read at your depth
The practical view
Go generics (Go 1.18+) let you write functions/types parameterized by type: func Map[T any](s []T, f func(T) T) []T. The compiler instantiates generic code per type. Two implementation strategies exist: stenciling (specialize the code for each type — fast, but more code) and dictionary passing (pass type information as arguments — less code, small runtime cost). Go uses a hybrid: it 'monomorphizes' by GC shape (types with the same size and pointer structure share one instantiation) and passes a dictionary for operations that depend on the exact type (method calls, conversions).
The same idea in other frameworks
N/A — Go compiler concern.N/A — Go concern.N/A — Go concern.N/A — Go concern.Legacy vs modern
Interface-based generic-ish code vs true generics
Using interface{} + type assertions is slower and less safe; generics specialize per shape and only pay dictionary costs where the type truly matters.
func Map(s []interface{}, f func(interface{}) interface{}) []interface{} // ❌ OLDfunc Map[T any](s []T, f func(T) T) []T { // ✅ NEW: type-safe
out := make([]T, len(s))
for i, v := range s { out[i] = f(v) }
return out
}Interview gotchas
Context
This tests the implementation strategy — interviewers want the code-size vs specialization trade-off and what 'shape' means (size + pointer layout).
The mechanical answer
GC shape stenciling groups types by their GC shape: types with the same size and pointer layout (e.g. int and int32 on a 64-bit platform, or two distinct structs with identical layout) are treated as one shape and get ONE specialized instantiation of the generic function. This bounds code bloat — the binary doesn't contain a copy per concrete type (as full C++-style monomorphization would). It also keeps the fast path fast: within a shape, the code is specialized (no interfaces, no indirection), so numeric/pointer-heavy generics perform like hand-written code. The cost: operations that depend on the EXACT type (not just the shape) can't be fully specialized, so the compiler passes a dictionary to supply the type-specific operations. This hybrid is the Go team's chosen balance between binary size, compile time, and runtime performance.
Trap
The naive answer is 'Go generates one copy per type'. It generates one per GC SHAPE, which is coarser than per type. Another trap: assuming generics are free — type-dependent operations (method calls from constraints, == on non-scalars) go through the dictionary indirection. Candidates who explain the shape grouping and dictionary fallback show real understanding.
Context
This probes the dictionary-passing implementation — interviewers want to know which operations require type-specific data that stenciling by shape can't provide.
The mechanical answer
The dictionary is a compile-time-generated data structure containing type-specific values needed at runtime: the itab for an interface method call required by a constraint, the descriptor for a conversion, or the comparison function for == on non-scalar types. It's passed as a hidden argument to the generic function (or to specific helpers), so the single shape-stenciled code body can consult it when it must behave differently per exact type. The compiler decides what goes in the dictionary: for a constraint like Comparable with a == operation, scalar types may be handled directly by shape, but struct types need a runtime equality function — that function pointer goes in the dictionary. This keeps the instantiation shared while still dispatching correctly per type.
Trap
The naive answer is 'dictionaries are just method tables'. They carry any type-specific operation (equality, conversions), not only methods. Another trap: assuming every generic call pays the dictionary cost — scalar/shape-covered operations are specialized and direct; only type-dependent operations dereference the dictionary. Candidates who describe the hidden dictionary argument and its contents show real depth.