Generics arrived in Go 1.18 (March 2022). They let you write functions and types that work on a parametric set of types, with compile-time type checking.
Type parameters
A type parameter is declared in square brackets after the function or type name:
func Max[T int | float64 | string](a, b T) T {
if a > b {
return a
}
return b
}
fmt.Println(Max(1, 2)) // T inferred as int
fmt.Println(Max(1.5, 0.5)) // T inferred as float64
fmt.Println(Max("a", "b")) // T inferred as string
The thing inside the brackets after the T is called a constraint.
Constraints
A constraint is just an interface. It can list methods, like a regular interface, and type sets:
// matches any integer type
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
// the leading ~ means "or any type whose underlying type is X",
// so `type Celsius float64` also satisfies a ~float64 constraint.
The standard library ships golang.org/x/exp/constraints
with Ordered, Integer, Float, Signed, Unsigned. You can use it
directly:
import "golang.org/x/exp/constraints"
func Min[T constraints.Ordered](a, b T) T {
if a < b { return a }
return b
}
Built-in constraints you'll see often:
any(alias forinterface{}).comparable— types you can use with==and!=. Required for map keys and for any generic that compares values.
func Index[T comparable](s []T, x T) int {
for i, v := range s {
if v == x { return i }
}
return -1
}
Generic types
type Stack[T any] struct {
data []T
}
func (s *Stack[T]) Push(v T) { s.data = append(s.data, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.data) == 0 { return zero, false }
v := s.data[len(s.data)-1]
s.data = s.data[:len(s.data)-1]
return v, true
}
Used as:
s := Stack[int]{}
s.Push(1); s.Push(2)
v, ok := s.Pop()
You'll see this pattern again in Part 2.
When to use generics
- Containers (
Stack[T],Set[T],Cache[K,V]). - Algorithms over types that share an operator (
Min,Max,Sum). - Function utilities (
Map,Filter,Reduce).
When not to use them:
- If a single concrete type is enough, don't reach for generics.
- If you only need behavior, an interface is simpler and clearer.
- Generic code is harder to read; reserve it for genuine reuse.
Type inference and explicit instantiation
The compiler infers T from the arguments when possible:
Max(1, 2) // inferred
Max[float64](1, 2) // explicit, here it forces conversion
For generic types you must instantiate:
s := Stack[int]{}
Method sets and generics
You cannot add type parameters to methods of an existing type. Only the type itself can have them. This means common patterns like generic visitor methods don't work directly — you usually convert a method to a function.
Run the example
go run ./part1-language/examples/09_generics