Under The Hood
backend go Go 1.21+ (generics)

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

react equivalent
N/A — Go compiler 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.

before → after
interface{} + type assertions
func Map(s []interface{}, f func(interface{}) interface{}) []interface{} // ❌ OLD
Type-parameterized generic function
func 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.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.