Go Generics: Type Parameters Complete Guide
Generics in Go
Go 1.18 introduced generics — type parameters in functions and data structures — in February 2022, marking the largest language change since Go’s initial 2009 release and the most significant addition to the type system. Generics allow writing type-safe, reusable code without interface{} type assertions, reflection, or code generation. The design was led by Robert Griesemer and Ian Lance Taylor following the Type Parameters Proposal (golang.org/design/43651-type-parameters), which went through three major revisions based on community feedback. Key design decisions include: square brackets for type parameters (distinct from C++ angle brackets to avoid parsing ambiguity), interface constraints that can specify both methods and type sets (using the | operator), full type inference in most call sites, and monomorphization (also called “stenciling”) for zero-cost abstraction — the compiler generates separate machine code for each concrete type, eliminating runtime dispatch overhead. The slices and maps standard library packages (Go 1.21+) provide generic functions for common operations including slices.Sort, slices.Index, slices.Compact, slices.Clone, maps.Keys, maps.Values, and maps.Clone. The cmp package provides the Ordered constraint and comparison utilities.
Basic Type Parameters
Type parameters are declared in square brackets before the function’s regular parameters:
func Index[T comparable](s []T, x T) int {
for i, v := range s {
if v == x { return i }
}
return -1
---
si := Index([]int{10, 20, 30, 40}, 30) // 2
ss := Index([]string{"a", "b", "c"}, "b") // 1The compiler infers type arguments from function arguments whenever possible. Explicit type arguments are only required when the type parameter does not appear in the function parameters. For example, func New[T any]() T { return *new(T) } requires explicit type argument: x := New[int]().
Multiple Type Parameters
Functions and types can have multiple type parameters:
func Map[T, U any](s []T, f func(T) U) []U {
r := make([]U, len(s))
for i, v := range s { r[i] = f(v) }
return r
---
type Pair[T, U any] struct {
First T
Second U
---Constraints
Constraints define the set of types a type parameter can accept. Custom constraints use interface syntax with type unions:
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
---
type Float interface { ~float32 | ~float64 }
type Ordered interface { Integer | Float | ~string }
func Sum[T Numeric](values []T) T {
var sum T
for _, v := range values { sum += v }
return sum
---
func Max[T Ordered](a, b T) T {
if a > b { return a }
return b
---The tilde (~) notation: ~int matches any type whose underlying type is int, including type aliases like type MyInt int. This enables generic functions to work with custom types sharing the same underlying representation. Without the tilde, only the exact type matches.
Method Constraints
Constraints can require methods in addition to type sets:
type Stringer interface { String() string }
func PrintAll[T Stringer](items []T) {
for _, item := range items { fmt.Println(item.String()) }
---When a constraint includes both type union terms and method requirements, the methods must be implemented by all types in the union. This combined constraint syntax is unique to Go and enables precise generic programming.
Generic Data Structures
Generics shine for type-safe data structures:
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
---Generic binary trees, graphs, and priority queues follow the same pattern. The key advantage is compile-time type safety: a Stack[int] cannot accidentally hold strings.
Performance Implications
Generics have zero runtime overhead compared to hand-written type-specific code. The compiler monomorphizes generic functions, generating separate machine code for each concrete type. This increases binary size slightly but eliminates the boxing and dynamic dispatch overhead of interface-based approaches. For hot code paths, generics can be faster than interfaces because the compiler can inline and optimize concrete types without indirect calls.
Real-World Generics Use Cases
The Go standard library adopted generics in Go 1.21 with the slices and maps packages, providing type-safe sort, search, and manipulation functions. Third-party libraries have followed: lo provides generic utility functions like Filter, Map, Reduce, and Uniq for slices; samber/mo offers Option, Either, and Result monad types; google/uuid uses generics for typed UUID wrappers. In production code, generics excel for collection types (type-safe sets, ordered maps, priority queues), pipeline patterns (typed channels with transform functions), and repository layers (generic CRUD with type-safe queries). A common pattern is the generic repository interface: type Repository[T any] interface { Get(ctx, id) (T, error); List(ctx) ([]T, error); Save(ctx, T) error }. This eliminates repetitive interface definitions for each entity type. The cmp.Ordered constraint enables generic sorting and comparison utilities across all ordered types. For JSON serialization, generic wrapper types can add custom marshaling behavior without modifying the underlying struct.
Constraints with Method Requirements
Go generics constraints combine type sets with method requirements, enabling precise type parameter restrictions. A constraint can require that types implement both String() and satisfy an underlying type constraint: type PrintableInt interface { ~int; String() string }. This allows generic functions that operate on custom integer types with formatting capabilities. The golang.org/x/exp/constraints package provides standard constraints including Complex, Float, Integer, Ordered, and Signed. Custom constraints should be defined in a shared package to avoid duplication across codebases. Constraint inference allows omitting explicit type arguments in most cases, but ambiguous calls may require explicit instantiation: GenericFunc[MyType](arg). The any constraint (equivalent to interface{}) allows generic functions that can accept any type but cannot perform operations on the value without type assertions. For zero-value initialization, var zero T or return *new(T) provides the zero value for any type parameter. The comparable constraint is a built-in interface that allows == and != comparisons, but not ordering operators.
Real-World Use Cases and Patterns
Generics solve several concrete problems that previously required code generation or interface{} dance. Collection types benefit most: a generic Stack[T] or Queue[T] provides type safety without boxing overhead. Generic utility functions like Map[T any]([]T, func(T) T) []T and Filter[T any]([]T, func(T) bool) []T eliminate the need for reflection-based collection manipulation. The slices and maps packages in the standard library (golang.org/x/exp/slices, golang.org/x/exp/maps) provide generic sort, search, and clone operations.
Data structure implementations are cleaner with generics: binary trees, graphs, and LRU caches avoid interface{} assertions. The cmp.Ordered constraint enables comparison-based data structures. Generic SQL row scanning eliminates type assertions in database code: func ScanRow[T any](row *sql.Row) (T, error). JSON unmarshaling into generic containers: func ParseJSON[T any](data []byte) (T, error). The functional options pattern combined with generics enables builder APIs that accept different option types while maintaining compile-time safety. Error handling benefits from Result[T, E] patterns inspired by Rust, though Go lacking sum types makes this less ergonomic. The key insight is that generics shine for data structures and algorithms, not for business logic abstraction.
Structural Type Constraints
Go 1.18+ generics support structural typing through type sets in constraints. A constraint like type Number interface { ~int | ~float64 } matches any type whose underlying type is int or float64. The tilde prefix is critical: ~int matches int, MyInt, and any other type with underlying int, while bare int matches only int exactly. Type sets can combine multiple underlying types and method signatures: type Sortable interface { ~[]T; Less(i, j int) bool }.
Interface-based constraints work the same as regular interfaces but applied at compile time. The constraints experiment package provides common constraints. For complex constraints, define them in a shared package to avoid duplication. Type inference deduces type parameters from arguments: Min(1, 2) infers T=int without explicit annotation. The type parameter is unavailable at runtime due to erasure — use reflect.TypeOf over a value, not over the type parameter. Function type parameters cannot be used with go:linkname or in non-generic function signatures. The any constraint is equivalent to interface{} and should only be used when truly any type is acceptable. For numeric operations, define a local constraint or use the golang.org/x/exp/constraints package.
Frequently Asked Questions
What is the difference between generics and interfaces? Generics provide compile-time polymorphism with zero runtime overhead via monomorphization. Interfaces provide runtime polymorphism with dynamic dispatch.
Does Go support generic methods on non-generic types? No — method type parameters are not supported. Only the receiver type can be generic.
What are the performance implications of generics? Zero runtime overhead compared to hand-written type-specific code. Binary size increases slightly due to monomorphization.
What is the tilde (~) in constraint syntax? The tilde allows the constraint to match types whose underlying type matches the specified type.
How do I use generics with sort or binary search? Go 1.21+ provides slices.Sort, slices.BinarySearch, and more generic functions in the standard library.
Related: Go Modules and Dependencies | Go JSON Serialization | Go Testing Frameworks