LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 4020 min read

Generics

Learn Go generics: type parameters introduced in Go 1.18, writing generic functions like Max and Filter, and the basics of type constraints.

Introduction

For its first decade, Go had no way to write a function that worked across multiple types without either duplicating code or falling back on interface{} and losing type safety. Generics, introduced in Go 1.18, closed that gap: you can now write a single function or type that works with any type satisfying a given constraint, while the compiler still checks everything at compile time.

What You Will Learn
  • Why generics matter and what problem they solve.
  • The syntax for declaring type parameters on functions and types.
  • How to write a generic Max function that works across numeric types.
  • What type constraints are and how to use the standard constraints package.
  • How to write a generic Filter function using constraints.

Why Generics?

Before generics, a function like Max had to be duplicated per type (MaxInt, MaxFloat64, ...) or written against interface{}, which loses compile-time type checking and requires runtime type assertions. Generics let you write the logic once, parameterized by type, with the compiler enforcing correctness for every concrete type you use it with.

Before generics: duplicated per type
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
func MaxFloat64(a, b float64) float64 {
if a > b {
return a
}
return b
}

Type Parameter Syntax

A type parameter is declared in square brackets right after the function name, along with a constraint describing what types are allowed. any is a built-in alias for interface{} and is the loosest possible constraint.

func Print[T any](value T) {
fmt.Println(value)
}
// Called with any type — Go infers T automatically:
Print(42)
Print("hello")
Print(3.14)

A Generic Max Function

Here is a single Max function that works for any ordered type — ints, floats, and strings alike — using the built-in cmp.Ordered constraint from the standard library.

package main
import (
"cmp"
"fmt"
)
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(3, 7)) // works with int
fmt.Println(Max(2.5, 1.1)) // works with float64
fmt.Println(Max("go", "rust")) // works with string
}
Output

Click Run to see what this code prints.

cmp.Ordered constrains T to any type that supports the <, >, <=, >= operators — integers, floats, and strings. The compiler checks this at compile time for every call site, so passing an unordered type like a struct would be a compile error, not a runtime one.

Type Constraints

A constraint is just an interface that lists which types (or which methods) are allowed to satisfy a type parameter. You can define your own constraints using a union of types with the | operator.

type Number interface {
int | int64 | float32 | float64
}
func Sum[T Number](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}
package main
import "fmt"
type Number interface {
int | int64 | float32 | float64
}
func Sum[T Number](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}
func main() {
ints := []int{1, 2, 3, 4}
floats := []float64{1.5, 2.5, 3.0}
fmt.Println(Sum(ints))
fmt.Println(Sum(floats))
}
Output

Click Run to see what this code prints.

A Generic Filter with the constraints Package

Generics really shine for utility functions that operate on slices of any type. Here is a generic Filter that returns only the elements matching a predicate, working for any element type T.

package main
import "fmt"
func Filter[T any](items []T, keep func(T) bool) []T {
result := make([]T, 0, len(items))
for _, item := range items {
if keep(item) {
result = append(result, item)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8}
evens := Filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens)
words := []string{"go", "java", "rust", "c"}
longWords := Filter(words, func(w string) bool {
return len(w) > 2
})
fmt.Println(longWords)
}
Output

Click Run to see what this code prints.

One Filter function now works for both []int and []string (and any other slice type) without any duplication or loss of type safety — exactly the kind of code generics were designed to eliminate.

Common Mistakes

Avoid These Mistakes
  • Overusing generics for code that only ever needs to work with one concrete type.
  • Using any when a narrower constraint (like cmp.Ordered or a custom union) would give better compile-time safety.
  • Assuming generic functions are automatically as fast as hand-written per-type code — they are close, but not always identical.
  • Reinventing constraints that already exist in the standard cmp or slices packages.
  • Forgetting that a type parameter is inferred from arguments — writing Max[int](3, 7) when Max(3, 7) already works.

Best Practices

  • Reach for generics when you find yourself duplicating identical logic across multiple types.
  • Prefer the narrowest constraint that actually describes what your function needs.
  • Use the standard library's cmp and slices packages before writing your own generic helpers.
  • Keep type parameter names short and conventional, like T, K, V, matching Go idiom.
  • Let Go infer type parameters from arguments instead of specifying them explicitly when possible.

Frequently Asked Questions

Generics landed in Go 1.18, released in March 2022, after several years of community design work and proposals.

No, they solve different problems. Interfaces describe behavior (a set of methods) for runtime polymorphism; generics parameterize code by type for compile-time reuse. They are often used together.

Generally minimal. The Go compiler generates efficient code for generic functions, though in some cases it can be marginally slower than fully specialized hand-written code — profile if performance is critical.

Key Takeaways

  • Generics (Go 1.18+) let a single function or type work across multiple types safely.
  • Type parameters are declared in square brackets with a constraint, e.g. func Max[T cmp.Ordered](...).
  • A constraint is an interface describing which types are allowed, often a union like int | float64.
  • any is the loosest constraint, equivalent to interface{}.
  • Generics are ideal for reusable utilities like Max, Sum, and Filter that work identically across types.

Summary

Generics give Go the ability to write reusable, type-safe code without duplicating logic per type or giving up compile-time checking. With Max, Sum, and Filter as examples, you have seen the core pattern you will reuse constantly in real Go codebases. Next, you will look more closely at managing third-party dependencies with Go modules.

Next Lesson →

Go Modules & Dependencies