Life Without Generics
Suppose you need to write a function that sums values: one version for int, and another for float64:
func SumInts(nums []int) int { var total int for _, n := range nums { total += n } return total}
func SumFloats(nums []float64) float64 { var total float64 for _, n := range nums { total += n } return total}The two functions are identical except for their types. To avoid duplication, the usual option was any:
// Use any (that is, interface{}) as input. The cost is that you need a type assertion every time, and errors only blow up at runtime.func SumAny(nums []any) any { var total int for _, n := range nums { total += n.(int) // Passing in a float64 will panic } return total}Your First Generic Function
Generics are like parameters for types, written in square brackets after the function name:
func Sum[T int | float64](nums []T) T { var total T for _, n := range nums { total += n } return total}[T int | float64]: declares a type parameterT; the part after it is the constraint, meaningTcan only beintorfloat64.nums []T, return valueT: inside the function signature, you can useTlike any ordinary type.var total T: the zero value ofT. Forint, it is0; forfloat64, it is0.0.
When calling the function, you usually do not need to write anything extra. The compiler infers T from the arguments:
Sum([]int{1, 2, 3}) // T is inferred as int, result is 6Sum([]float64{1.5, 2.5}) // T is inferred as float64, result is 4Sum[int]([]int{1, 2, 3}) // You can also specify it manually
Sum([]string{"a", "b"}) // Compilation error: string is not in the constraintThe error happens at compile time, not runtime. This is the biggest difference between generics and using any with type assertions.
How to Write Constraints
A constraint is essentially an interface, except it describes not only methods, but also “which types can be substituted.”
any: Any Type Is Allowed
When any is used as a constraint, it means there is no type restriction. But because of that, you cannot make any assumptions about the value; you can only move it around, compare pointers, or put it into containers. A classic example is Map:
func Map[T, U any](s []T, f func(T) U) []U { result := make([]U, 0, len(s)) for _, v := range s { result = append(result, f(v)) } return result}
names := Map([]int{1, 2, 3}, func(n int) string { return fmt.Sprintf("no.%d", n)})// [no.1 no.2 no.3]A function can have multiple type parameters. Here, T is the input element type, U is the output element type, and the two are connected by the signature of f.
comparable: Can Be Compared with ==
If you want to write == or != inside a function, the constraint must be comparable:
func Contains[T comparable](s []T, target T) bool { for _, v := range s { if v == target { return true } } return false}
Contains([]string{"a", "b"}, "b") // truecomparable covers numbers, strings, booleans, pointers, channels, and structs and arrays whose fields are all comparable. Slices, maps, and funcs are not included, because they cannot be compared with == in the first place.
cmp.Ordered: Can Be Compared with < >
comparable only guarantees ==. To compare ordering, use cmp.Ordered from the standard library’s cmp package:
import "cmp"
func Max[T cmp.Ordered](s []T) (T, bool) { var zero T if len(s) == 0 { return zero, false } m := s[0] for _, v := range s[1:] { if v > m { m = v } } return m, true}
Max([]int{3, 1, 4}) // 4, trueMax([]string{"b", "a", "c"}) // "c", trueCustom Constraints and the ~ Symbol
When constraints get longer, extract them into a named interface:
type Number interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~float32 | ~float64}
func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total}The tilde in ~int is read as “all types whose underlying type is int.” The difference is here:
type Celsius float64
Sum([]Celsius{36.5, 37.2}) // This only passes if the constraint is ~float64; float64 alone will reject itWithout ~, only float64 itself counts; custom types like Celsius are excluded. In practice, when writing constraints, adding ~ by default is usually the right choice.
Constraints can also include methods, in which case they are no different from ordinary interfaces:
type Stringer interface { String() string}
func JoinAll[T Stringer](items []T) string { parts := make([]string, 0, len(items)) for _, item := range items { parts = append(parts, item.String()) } return strings.Join(parts, ", ")}Generic Types
In addition to functions, structs can also have type parameters. For example, here is a type-safe Stack:
type Stack[T any] struct { items []T}
func NewStack[T any]() *Stack[T] { return &Stack[T]{}}
func (s *Stack[T]) Push(item T) { s.items = append(s.items, item)}
func (s *Stack[T]) Pop() (T, bool) { var zero T if len(s.items) == 0 { return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}
func (s *Stack[T]) Len() int { return len(s.items)}
// Starting with Go 1.27, methods can also declare their own type parameters.func (s *Stack[T]) MapTo[U any](f func(T) U) *Stack[U] { result := NewStack[U]() for _, item := range s.items { result.Push(f(item)) } return result}Using it looks like this:
s := NewStack[string]()s.Push("a")s.Push("b")
v, ok := s.Pop() // "b", trues.Push(42) // Compilation error: the type has already been fixed as string
lengths := s.MapTo(func(v string) int { return len(v) })lengths.Push(42) // OK, lengths is *Stack[int]- When creating a type instance, you cannot omit the type parameter.
&Stack{}is invalid; you must write&Stack[string]{}. This is why it is common to pair the type with aNewStack[T]()constructor so inference can take effect. - The method receiver must include
[T]. Starting with Go 1.27, methods themselves can also declare new type parameters, such asMapTo[U any]in the example above; in older versions of Go, this kind of requirement could only be written as a standalone function. - Interface methods still cannot declare type parameters, and a generic method cannot be used to implement an interface method.
The Standard Library Has Already Written This for You
The Contains and Max demonstrated above do not actually need to be implemented manually. The generic packages slices and maps already cover most everyday needs:
import ( "maps" "slices")
nums := []int{3, 1, 4, 1, 5}
slices.Contains(nums, 4) // trueslices.Index(nums, 4) // 2slices.Max(nums) // 5slices.Sort(nums) // Sorts in place to [1 1 3 4 5]slices.Reverse(nums)
people := []Person{{Name: "b"}, {Name: "a"}}slices.SortFunc(people, func(x, y Person) int { return cmp.Compare(x.Name, y.Name)})
m := map[string]int{"a": 1, "b": 2}keys := slices.Collect(maps.Keys(m)) // maps.Keys returns an iteratorslices.Sort(keys) // Map iteration order is random; sort it yourself if neededWhen Not to Use Generics
Generics solve the problem of “applying the same logic to multiple types,” not “creating unnecessary abstractions”:
- If only one type will use it, do not make it generic. Wait until a second type really appears before refactoring. The Go convention has always been to duplicate first and abstract later.
- If you only need to call methods, use a regular interface. For something like
func Print(s fmt.Stringer), writing it asfunc Print[T fmt.Stringer](s T)gives you no benefit; it is just more typing. The difference is that generics preserve the concrete type (you can returnTor put it into[]T), while an ordinary interface erases it. - If the logic varies by type, do not force it into generics. If type-based branching starts appearing inside the function, that means these are really two separate functions.
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| Type parameter | func F[T any](...) | Turns a type into a parameter |
| Type inference | F(v) instead of F[int](v) | Avoids manual specification in most cases |
any | [T any] | Allows any type, but you cannot make assumptions about the value |
comparable | [T comparable] | Allows ==, != |
cmp.Ordered | [T cmp.Ordered] | Allows <, > |
~ | ~int | ~float64 | Covers custom types with the same underlying type |
| Generic type | type Stack[T any] struct | Type-safe containers |
Type parameters go in square brackets; constraints determine what you can do with a value
Further Reading
- Tutorial: Getting started with generics - go.dev
- An Introduction To Generics - The Go Blog
- When To Use Generics - The Go Blog
- slices package - pkg.go.dev