GAP·MAP

Go generics

[ middle depth ]

Type parameters on functions and types

Since Go 1.18, a function or a named type can take type parameters: a bracketed list right after the name, before the ordinary parameters.

func Map[T, U any](s []T, f func(T) U) []U { // generic function
	out := make([]U, len(s))
	for i, v := range s {
		out[i] = f(v)
	}
	return out
}

type Stack[T any] struct{ items []T } // generic type

Every type parameter needs a constraint; write any when you want no restriction. A generic type has to be instantiated before you can use it as a type, so var s Stack[int] is valid and var s Stack is not. Only functions and named types can be generic.

Constraints are interfaces with a type set

A constraint is an interface. Besides a method set, an interface now also names a set of permitted types, and that set decides which operations are legal on a value of the type parameter.

type Number interface{ ~int | ~int64 | ~float64 } // a type set
func Sum[T Number](xs []T) T {
	var total T
	for _, x := range xs {
		total += x // + is legal because every type in the set supports it
	}
	return total
}

any is the no-restriction constraint, an alias for interface{}. To compare with == you need comparable; a plain [T any] does not permit ==, <, or arithmetic. A type parameter can do only what its constraint's type set allows.

Type inference at the call site

Usually you leave the type arguments off and let the compiler read them from the value arguments.

m := Map([]int{1, 2, 3}, strconv.Itoa) // T=int, U=string inferred

Inference works only for type parameters that appear in the ordinary parameters. One that shows up only in the result or the body cannot be inferred, so func New[T any]() T has to be called as New[int]().

Generics or an interface?

Reach for generics when you write the same code across many element types: container helpers over slices, maps, and channels, or data structures like sets and trees that store T directly rather than any.

Wrong "Type parameters are the faster, modern replacement for interface parameters." If a function only calls a method on its argument, take the interface (io.Reader, not [T io.Reader]). It reads better and runs no faster. Interfaces remain the tool for runtime polymorphism, where the implementation genuinely differs per type.

[ senior depth ]

No generic methods

A method may reuse the type parameters of its receiver, but it cannot declare new ones of its own.

type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(v T) { s[v] = struct{}{} } // reuses the receiver's T
// func (s Set[T]) Map[U any](f func(T) U) ...   // compile error: methods have no type parameters

So a type-parameterized operation has to be a top-level function, and you cannot place one in an interface. That is why the standard slices and maps packages (Go 1.21) are functions rather than methods on the container types. The receiver's parameters in Set[T] are binding identifiers, not instantiations.

The ~ token and underlying types

A bare type term matches that exact type. ~T matches every type whose underlying type is T.

type Celsius float64
type Exact interface{ float64 }  // bare term: matches only float64
type Under interface{ ~float64 } // ~ term: matches Celsius too

The rule has teeth: in ~T, the underlying type of T must be T itself, and T cannot be an interface. So ~Celsius and ~error do not compile, while ~int and ~[]byte do. Dropping the ~ is the usual bug: a constraint written int | string silently rejects every defined type built on int or string.

comparable no longer promises a panic-free ==

comparable is a predeclared constraint (Go 1.18) for the strictly comparable types, the ones safe as map keys. Since Go 1.20 a non-strictly-comparable type such as any also satisfies comparable, even though it does not implement it.

func Index[T comparable](s []T, x T) int { /* uses v == x */ }
Index([]any{[]int{1}}, []int{1}) // compiles, panics at run time on []int == []int

Generic code over comparable can therefore panic on ==, which was impossible before 1.20; the compiler admits the type argument and the runtime carries the risk. There is no ordered predeclared identifier: ordering comes from cmp.Ordered (standard library, Go 1.21) or constraints.Ordered in the experimental golang.org/x/exp/constraints.

GC-shape stenciling, not monomorphization

Go does not emit one specialized copy per type argument the way C++ and Rust do. It stencils one copy per GC shape (a type's size, alignment, and pointer layout) and threads a hidden dictionary of type information into each call. All pointer types share a single shape, so Stack[*Foo] and Stack[*Bar] run the same generated code through that dictionary.

Wrong "Generics compile to specialized code, so they beat interfaces on speed." A pointer-shaped instantiation goes through the dictionary and an indirection, so its cost is often close to an interface rather than a monomorphized speedup. One hard limit falls out of the model: dictionaries are built at compile time, so a program whose recursion would spawn unboundedly many distinct instantiations is rejected.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more Go

was this useful?