Generics Implementation Deep Dive
Generics Implementation Deep Dive
Overview
Go generics (1.18+) are implemented with a hybrid strategy often described as dictionaries + partial stenciling (GC shape based). You do not need every compiler paper — you need the cost model: when generics are zero-cost abstraction vs when they introduce runtime dictionary calls or more code size.
Usage: Generics intro through Idiomatic generics.
Diagram: Generic call shapes
generic source
│
v
typecheck + constraints
│
v
GC shape groups + dictionaries
│
v
shared/specialized machine code
What the Compiler Sees
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}Calls with different type arguments may share generated code when the GC shape matches (same underlying representation for pointers, etc.), parameterized by a dictionary describing operations (comparison, conversion, …).
source generic
|
v
type check + capture constraints
|
v
shape grouping (pointer-ish shapes share)
|
v
code + dictionary for operations on T
Cost Dimensions
| Dimension | Effect |
|---|---|
| Code size | Too many distinct shapes / specialized methods → binary growth |
| Call cost | Dictionary-indirect calls can inhibit inlining |
| Allocation | Generic APIs that box to any lose the point |
| Readability | Complex constraint graphs hurt more than they help |
Generics excel for:
- Containers and algorithms over
~[]T, maps, comparables - Removing interface{} while keeping type checks
Generics struggle when:
- Every method needs unique runtime behavior per type without structure
- You force abstraction that a plain function + concrete types would inline better
Constraints Are Interfaces
type Number interface {
~int | ~int64 | ~float64
}Constraint satisfaction is compile-time. Method sets on type parameters follow the constraint’s method set.
Instantiation
f := Max[int] // function value of specialized shape
v := Max(1, 2) // inferenceType inference fails when the compiler cannot unify arguments — add explicit type args rather than fighting it.
Interaction With Interfaces
func Print[T fmt.Stringer](v T) {
fmt.Println(v.String())
}Prefer type parameters when callers should keep static types through the call graph; prefer interfaces when you need heterogeneous collections or runtime plugin style.
Avoid These Footguns
- Generic + reflect by default — pays both complexity taxes.
- Over-abstracting one-use helpers — a function with
intis fine. - Exporting huge generic surface — every instantiation pressure multiplies for users.
Experiment
go mod init example
go run .
go build -gcflags='-m' . 2>&1 | head -40package main
import (
"cmp"
"fmt"
)
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
type Pair[K comparable, V any] struct {
K K
V V
}
func (p Pair[K, V]) String() string {
return fmt.Sprintf("%v=%v", p.K, p.V)
}
func main() {
fmt.Println(Max(3, 5))
fmt.Println(Max(2.5, 1.2))
fmt.Println(Pair[string, int]{K: "n", V: 1})
}What to notice: One source definition serves multiple shapes. -gcflags=-m may show inlining decisions around simple generic functions.
Try next: Compare assembly of Max[int] vs a non-generic maxInt with go build -gcflags=-S and see if dictionary calls appear in your version.