Generics & Type Parameters
Irbisa · cheatsheetSeptember 19, 2026

Generics & Type Parameters

Type parameters, constraints, type sets, inference, GC shape stenciling

Middle Developer16 itemscompressed for a skim
  1. 01

    Calling func Join[T string](parts []T, sep T) T with a []UserID, where type UserID string, fails: UserID does not satisfy string. The helper itself compiles. What is missing?

    Easy

    The constraint names the type string itself, not its underlying type — ~string is the approximation element that admits every named type whose underlying type is string.

    type UserID string
    
    // ❌ the type set is exactly {string}; UserID is a different named type
    func JoinBad[T string](parts []T, sep T) T {
    	var out T
    	for i, p := range parts {
    …
  2. 02

    In a generic Find, reporting "nothing matched" needs an actual return value, but return nil, false does not compile and return 0, false only works when T is numeric. What do you write?

    Easy

    var zero T — declare a variable of the type parameter and return it; that is the only way to spell the zero value of a type you do not know yet.

    // Find returns the first match. The bool is the answer; the T is only the payload.
    func Find[T any](xs []T, ok func(T) bool) (T, bool) {
    	for _, x := range xs {
    		if ok(x) {
    			return x, true
    		}
    …
  3. 03

    A reviewer deletes your func Max[T constraints.Ordered](a, b T) T with "Go has that built in now", and then Max(scores) over a []int stops compiling. What did Go 1.21 actually add?

    Easy

    Go 1.21 made min, max and clear builtinsmin and max are variadic over arguments, not over a slice, so the slice version is slices.Max.

    type Conn struct{ addr string }
    
    func demo(c *Conn) {
    	scores := []int{7, 3, 9}
    
    	// ❌ the builtins take arguments, not a slice
    …
  4. 04

    Map(users, nameOf) needs no type arguments, but NewSet() fails with cannot infer T even when the result is assigned straight to a *Set[string]. What is the rule?

    Easy

    Inference runs on the types of the actual arguments and on the constraints, never on what the result is assigned to — so a type parameter that appears only in the result type has to be written out: NewSet[string]().

    type Set[T comparable] struct{ m map[T]struct{} }
    
    func NewSet[T comparable]() *Set[T] { return &Set[T]{m: map[T]struct{}{}} }
    
    func Map[S, T any](xs []S, f func(S) T) []T {
    	out := make([]T, 0, len(xs))
    …
  5. 05

    An Index[T comparable](s []T, v T) int helper accepts a []any and compiles clean, then production panics with comparing uncomparable type []int. Is comparable not supposed to rule that out?

    Medium

    Since Go 1.20 an ordinary interface type such as any satisfies comparable without implementing it — the constraint only guarantees that == is legal to write, and for interface values the real check happens at run time, where a dynamic type of slice, map or func panics.

    func Index[T comparable](s []T, v T) int {
    	for i, x := range s {
    		if x == v { // compiles for T = any; the check is deferred to the runtime
    			return i
    		}
    	}
    …
  6. 06

    type Number interface{ ~int | ~float64 } works as a constraint, but var n Number fails with interface contains type constraints, and the same review asks you to drop the golang.org/x/exp/constraints import. What is going on?

    Medium

    An interface that carries a type set — anything with a union or a ~ term — is a constraint only and can never be the type of a value; you build the one you need by embedding other constraints, and the Ordered everyone imported x/exp for has been cmp.Ordered in the standard library since Go 1.21.

    type Integer interface {
    	~int | ~int8 | ~int16 | ~int32 | ~int64
    }
    
    type Float interface{ ~float32 | ~float64 }
    …
  7. 07

    Both Cents and Micros declare a String() string method, yet inside func Label[T interface{ Cents | Micros }](v T) string the call v.String() does not compile. What may a union term be, and what survives into T?

    Medium

    A type parameter has exactly the methods in its constraint's method set, and a union contributes a type set only — the String that every member happens to have is invisible, so you embed fmt.Stringer alongside the union.

    type Cents int64
    type Micros int64
    
    func (c Cents) String() string  { return fmt.Sprintf("%d.%02d", c/100, c%100) }
    func (m Micros) String() string { return fmt.Sprintf("%d.%06d", m/1e6, m%1e6) }
    …
  8. 08

    Your leaderboard sorts with slices.SortFunc(rows, func(a, b Row) int { if a.Score < b.Score { return -1 }; return 1 }) and the test that asserts the order is flaky. What is wrong with the comparison?

    Medium

    It never returns 0, so two rows with the same score report both a < b and b < a — that is not a strict weak ordering, and slices.SortFunc is then free to produce any permutation.

    type Row struct {
    	Name  string
    	Score float64
    }
    
    func demo(rows []Row) {
    …
  9. 09

    Adding func (s *Set[T]) Map[U any](f func(T) U) *Set[U] to your set type is rejected with method must have no type parameters. Why is that a language rule rather than a missing feature, and what do you write instead?

    Medium

    A method with its own type parameters would need an unbounded method set — one entry per instantiation, and which instantiations exist is not knowable where an interface value is built — so Go permits type parameters only on the receiver's type.

    type Set[T comparable] struct{ m map[T]struct{} }
    
    func NewSet[T comparable](vs ...T) *Set[T] {
    	s := &Set[T]{m: make(map[T]struct{}, len(vs))}
    	for _, v := range vs {
    		s.m[v] = struct{}{}
    …
  10. 10

    Decoding through Decode[T Validator](b []byte) (T, error) refuses your Row with Row does not satisfy Validator (method Validate has pointer receiver), and Decode[*Row] then hands json.Unmarshal a nil pointer. How do you say "T, whose pointer has these methods"?

    Medium

    Take two type parameters[T any, PT interface{ *T; Validator }] — so the value stays a T while the method set comes from *T, which you reach inside the body with PT(&v).

    type Validator interface{ Validate() error }
    
    type Row struct{ N int }
    
    // pointer receiver: in *Row's method set, not Row's
    func (r *Row) Validate() error {
    …
  11. 11

    Swapping a small cache's []any for a []T dropped a benchmark that appends 1000 ints from 744 allocations per op to zero. Where did the allocations come from, and why 744 rather than 1000?

    Medium

    Putting a value into an any boxes it: an interface is a type word and a pointer, so a non-pointer value has to be copied to the heap.

    // ❌ every non-pointer element is copied to the heap and read back through an assertion
    type StackAny struct{ xs []any }
    
    func (s *StackAny) Push(v any) { s.xs = append(s.xs, v) }
    func (s *StackAny) PopInt() int {
    	v := s.xs[len(s.xs)-1]
    …
  12. 12

    You drop golang.org/x/exp/maps for the standard maps package and sort.Strings(maps.Keys(m)) stops compiling, complaining about iter.Seq[string]. What changed, and what is the one-liner now?

    Medium

    The standard maps.Keys returns an iter.Seq[K], a lazy iterator, where the x/exp version returned a []K.

    func demo() {
    	m := map[string]int{"b": 2, "a": 1, "c": 3}
    
    	// ❌ x/exp/maps.Keys returned []K; the standard one returns iter.Seq[K]
    	// sort.Strings(maps.Keys(m))
    	//   cannot use maps.Keys(m) (value of type iter.Seq[string]) as []string
    …
  13. 13

    Your Lines(name) returns an iter.Seq2[string, error]; a caller breaks out on the first malformed line and the file descriptor stays open. Where does cleanup belong in a range-over-function iterator?

    Hard

    In a defer inside the iterator function.

    // Cleanup belongs to the iterator: it opened the file, so it closes it.
    func Lines(name string) iter.Seq2[string, error] {
    	return func(yield func(string, error) bool) {
    		f, err := os.Open(name)
    		if err != nil {
    			yield("", err)
    …
  14. 14

    One colleague calls Go generics "templates, so they're free", another calls them "interfaces with nicer syntax". go tool nm shows a single main.First[go.shape.*uint8] but two symbols named main..dict.First[...]. What did the compiler actually do?

    Hard

    It emitted one function body per GC shape — types with the same size, alignment and pointer layout share a stencil — and gave each instantiation a hidden dictionary argument holding the type descriptors, itabs and sub-dictionaries the shared body cannot know statically.

    type A struct{ p *int }
    type B struct{ q *string }
    
    func First[T any](xs []T) T { return xs[0] }
    
    func Sum[T ~int | ~float64](xs []T) T {
    …
  15. 15

    func Scale[E ~int32](s []E, c E) []E accepts your type Point []int32 happily, but Scale(p, 2).Norm() fails — the result is a plain []int32. What signature keeps the named type, and why does the call site not change?

    Hard

    Parameterise the slice as well — func Scale[S ~[]E, E ~int32](s S, c E) S — and constraint type inference recovers E from S's core type, so callers still write Scale(p, 2) and get a Point back.

    type Point []int32
    
    func (p Point) Norm() int32 {
    	var n int32
    	for _, v := range p {
    		n += v * v
    …
  16. 16

    A PR collapses three 25-line repository types into one Repo[T any, ID comparable] that takes six function values as struct fields. The reviewer asks for the three concrete types back. On what grounds?

    Hard

    Generics pay only when the function body is genuinely identical for every type argument.

    // ❌ the type parameters are decoration: every difference between the three
    // repositories was pushed into a closure, so nothing was actually shared.
    type Repo[T any, ID comparable] struct {
    	table   string
    	scan    func(*sql.Rows) (T, error)
    	id      func(T) ID
    …