Generics & Type Parameters
Type parameters, constraints, type sets, inference, GC shape stenciling
- 01
Calling
func Join[T string](parts []T, sep T) Twith a[]UserID, wheretype UserID string, fails:UserID does not satisfy string. The helper itself compiles. What is missing?EasyThe constraint names the type
stringitself, not its underlying type —~stringis the approximation element that admits every named type whose underlying type isstring.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 { … - 02
In a generic
Find, reporting "nothing matched" needs an actual return value, butreturn nil, falsedoes not compile andreturn 0, falseonly works whenTis numeric. What do you write?Easyvar 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 } … - 03
A reviewer deletes your
func Max[T constraints.Ordered](a, b T) Twith "Go has that built in now", and thenMax(scores)over a[]intstops compiling. What did Go 1.21 actually add?EasyGo 1.21 made
min,maxandclearbuiltins —minandmaxare variadic over arguments, not over a slice, so the slice version isslices.Max.type Conn struct{ addr string } func demo(c *Conn) { scores := []int{7, 3, 9} // ❌ the builtins take arguments, not a slice … - 04
Map(users, nameOf)needs no type arguments, butNewSet()fails withcannot infer Teven when the result is assigned straight to a*Set[string]. What is the rule?EasyInference 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)) … - 05
An
Index[T comparable](s []T, v T) inthelper accepts a[]anyand compiles clean, then production panics withcomparing uncomparable type []int. Iscomparablenot supposed to rule that out?MediumSince Go 1.20 an ordinary interface type such as
anysatisfiescomparablewithout 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 } } … - 06
type Number interface{ ~int | ~float64 }works as a constraint, butvar n Numberfails withinterface contains type constraints, and the same review asks you to drop thegolang.org/x/exp/constraintsimport. What is going on?MediumAn 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 theOrderedeveryone imported x/exp for has beencmp.Orderedin the standard library since Go 1.21.type Integer interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } type Float interface{ ~float32 | ~float64 } … - 07
Both
CentsandMicrosdeclare aString() stringmethod, yet insidefunc Label[T interface{ Cents | Micros }](v T) stringthe callv.String()does not compile. What may a union term be, and what survives intoT?MediumA type parameter has exactly the methods in its constraint's method set, and a union contributes a type set only — the
Stringthat every member happens to have is invisible, so you embedfmt.Stringeralongside 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) } … - 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?MediumIt never returns
0, so two rows with the same score report botha < bandb < a— that is not a strict weak ordering, andslices.SortFuncis then free to produce any permutation.type Row struct { Name string Score float64 } func demo(rows []Row) { … - 09
Adding
func (s *Set[T]) Map[U any](f func(T) U) *Set[U]to your set type is rejected withmethod must have no type parameters. Why is that a language rule rather than a missing feature, and what do you write instead?MediumA 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
Decoding through
Decode[T Validator](b []byte) (T, error)refuses yourRowwithRow does not satisfy Validator (method Validate has pointer receiver), andDecode[*Row]then handsjson.Unmarshala nil pointer. How do you say "T, whose pointer has these methods"?MediumTake two type parameters —
[T any, PT interface{ *T; Validator }]— so the value stays aTwhile the method set comes from*T, which you reach inside the body withPT(&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
Swapping a small cache's
[]anyfor a[]Tdropped 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?MediumPutting a value into an
anyboxes 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
You drop
golang.org/x/exp/mapsfor the standardmapspackage andsort.Strings(maps.Keys(m))stops compiling, complaining aboutiter.Seq[string]. What changed, and what is the one-liner now?MediumThe standard
maps.Keysreturns aniter.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
Your
Lines(name)returns aniter.Seq2[string, error]; a callerbreaks out on the first malformed line and the file descriptor stays open. Where does cleanup belong in a range-over-function iterator?HardIn a
deferinside 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
One colleague calls Go generics "templates, so they're free", another calls them "interfaces with nicer syntax".
go tool nmshows a singlemain.First[go.shape.*uint8]but two symbols namedmain..dict.First[...]. What did the compiler actually do?HardIt 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
func Scale[E ~int32](s []E, c E) []Eaccepts yourtype Point []int32happily, butScale(p, 2).Norm()fails — the result is a plain[]int32. What signature keeps the named type, and why does the call site not change?HardParameterise the slice as well —
func Scale[S ~[]E, E ~int32](s S, c E) S— and constraint type inference recoversEfromS's core type, so callers still writeScale(p, 2)and get aPointback.type Point []int32 func (p Point) Norm() int32 { var n int32 for _, v := range p { n += v * v … - 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?HardGenerics 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 …