You write a generic helper with the loosest constraint you can, any, since it feels like it should accept anything. The moment the function body tries to compare two values of that type with ==, the package refuses to compile at all — not a runtime panic, a flat compile error, because any doesn't promise the type supports comparison.
func Contains[T any](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
Contains([]int{1, 2, 3}, 2)
// ./generics_bug.go:5:6: invalid operation: v == target (incomparable types in type set)Generics let a function or type declare one or more type parameters in square brackets, each constrained by an interface that specifies which types are allowed. Before Go 1.18 (released March 2022), writing a function that worked across multiple types meant either duplicating code per type or falling back to interface{} with runtime type assertions; generics give compile-time type safety instead, without sacrificing Go’s single-implementation style.
A generic function
A type parameter list in square brackets follows the function name, naming a placeholder type and its constraint. The built-in constraint any (an alias for interface{}) allows every type; numeric constraints restrict to types that support operators like <.
func Max[T int | float64](a, b T) T {
if a > b {
return a
}
return b
}
fmt.Println(Max(3, 7)) // 7 — T inferred as int
fmt.Println(Max(2.5, 1.1)) // 2.5 — T inferred as float64Constraints: which types are allowed
A constraint is just an interface, optionally listing specific allowed types with |. The standard library's constraints package (and the built-in comparable constraint, for types usable with ==) provide common ones, and you can declare your own.
type Number interface {
int | int64 | float32 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
fmt.Println(Sum([]int{1, 2, 3})) // 6
fmt.Println(Sum([]float64{1.5, 2.5})) // 4.0This is exactly the built-in comparable constraint mentioned above in action: it's the fix for the Contains compile error in the callout at the top of this page. Swapping any for comparable tells the compiler that every type T gets substituted with must support ==, which is the one thing Contains actually needs:
func Contains[T comparable](s []T, target T) bool {
for _, v := range s {
if v == target {
return true
}
}
return false
}
fmt.Println(Contains([]int{1, 2, 3}, 2)) // trueGeneric types, not just functions
Types can also take type parameters, which is how you'd write a single, reusable, type-safe container (a stack, a linked list, a set) instead of one implementation per type or falling back to interface{}.
type Stack[T any] struct {
items []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
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
var s Stack[string] // a Stack that only ever holds strings
s.Push("a")