Why Generics Matter
Overview
Generics (type parameters), introduced in Go 1.18, allow writing functions and types that work with multiple types while maintaining type safety.
The Problem Before Generics
// Without generics: duplicate code or use interface{}
func MinInt(a, b int) int {
if a < b { return a }
return b
}
func MinFloat(a, b float64) float64 {
if a < b { return a }
return b
}
// Or lose type safety
func Min(a, b interface{}) interface{} {
// Requires type assertions, runtime checks
}With Generics
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
Min(1, 2) // int
Min(1.5, 2.5) // float64
Min("a", "b") // stringType Parameters
func Print[T any](v T) {
fmt.Println(v)
}
// Multiple type parameters
func Pair[K, V any](k K, v V) (K, V) {
return k, v
}Constraints
import "golang.org/x/exp/constraints"
// Built-in constraints
any // No requirements
comparable // Supports == and !=
// From constraints package
constraints.Ordered // Supports < > <= >=
constraints.Integer // Int types
constraints.Float // Float typesCustom Constraints
type Number interface {
int | int32 | int64 | float32 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}Generic Type Aliases (Go 1.24+)
Go 1.24 introduced support for generic type aliases, allowing you to create aliases for generic types.
type Set[T comparable] map[T]struct{}
// Type alias (Go 1.24+)
type IntSet = Set[int]
type AnySet[T comparable] = Set[T]This is particularly useful for refactoring and maintaining compatibility while migrating to generic implementations.
Self-Referential Generics (Go 1.26+)
Go 1.26 lifted the restriction that prevented generic type parameters from referring to their enclosing type definition in constraint declarations. This allows fluent builder interfaces and self-referential tree nodes with static type safety:
// Self-referential constraint allowed in Go 1.26+
type Node[T Node[T]] interface {
Value() string
Next() T
}
type MyNode struct {
val string
next *MyNode
}
func (m *MyNode) Value() string { return m.val }
func (m *MyNode) Next() *MyNode { return m.next }Benefits
- Type safety: Errors caught at compile time
- No code duplication: One implementation for many types
- Performance: No interface boxing/unboxing
- Better documentation: Types are explicit
Summary
| Before | After |
|---|---|
| Code duplication | Single generic function |
interface{} |
Type parameters |
| Runtime errors | Compile-time errors |
| Type assertions | Direct usage |
More examples
Example: before generics—duplication
Save as main.go and go run . (with go mod init example if needed).
package main
import "fmt"
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func maxFloat(a, b float64) float64 {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(maxInt(3, 7))
fmt.Println(maxFloat(3.5, 2.1))
}Expected:
7
3.5
Example: one generic Max
Save as main.go and go run . (with go mod init example if needed).
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))
fmt.Println(Max(3.5, 2.1))
fmt.Println(Max("a", "z"))
}Expected:
7
3.5
z
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import (
"cmp"
"fmt"
)
// cmp.Ordered covers types that support < (stdlib; no x/exp needed).
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func Pair[K, V any](k K, v V) (K, V) {
return k, v
}
// Custom constraint with a type set (stdlib only).
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println("Min int:", Min(3, 1))
fmt.Println("Min float:", Min(2.5, 2.7))
fmt.Println("Min string:", Min("go", "ai"))
k, v := Pair("age", 30)
fmt.Printf("Pair: %s=%d\n", k, v)
fmt.Println("Sum ints:", Sum([]int{1, 2, 3}))
fmt.Println("Sum floats:", Sum([]float64{0.5, 1.5}))
}Expected output:
Min int: 1
Min float: 2.5
Min string: ai
Pair: age=30
Sum ints: 6
Sum floats: 2
What to notice: One Min works for any ordered type with compile-time safety; cmp.Ordered lives in the standard library. Type sets let you restrict Sum to numeric-ish types without interface{} assertions.
Try next: Call Min on a struct (should fail to compile); add ~string to a constraint and write Join[T ~string](parts []T) T.