Write the function once. Let the compiler figure out the types.
Estimated Reading Time : 10m
The problem generics solve
Before Go 1.18, writing a function that worked across multiple types meant either duplicating code or reaching for interface{} and casting at runtime.
func addInt(a, b int) int {
return a + b
}
func addFloat64(a, b float64) float64 {
return a + b
}
Two functions, identical logic. Generics let you collapse these into one.
func add[T int | float64](a, b T) T {
return a + b
}
The [T int | float64] part is the type parameter. T is a placeholder — at compile time, Go generates the concrete implementation for each type you actually use.
Type constraints
A type constraint defines the set of types a type parameter can be. In the example above, int | float64 is an inline constraint. For anything more involved, define a named interface:
type Number interface {
int8 | int16 | int | int64 | float32 | float64
}
func add[T Number](a, b T) T {
return a + b
}
This is the new role of interfaces in Go 1.18 — they define type sets, not just method sets. An interface used as a constraint can list concrete types, not just methods.
Predefined constraints
The golang.org/x/exp/constraints package ships a set of reusable constraints. constraints.Ordered covers all types that support <, >, <=, >= — signed and unsigned integers, floats, and strings:
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
func add[T constraints.Ordered](a, b T) T {
return a + b
}
func main() {
fmt.Println(add(-1, -2.3))
fmt.Println(add(1, 2.3))
fmt.Println(add("go", "1.18"))
}
$ go run main.go
-3.3
3.3
go1.18
Supporting type aliases with ~
By default, a constraint like int only matches int exactly. If you have a named type built on top of int, it won’t satisfy the constraint unless you prefix the type with ~:
type userID int
func double[T ~int](v T) T {
return v * 2
}
func main() {
var id userID = 5
fmt.Println(double(id)) // works
}
The ~int means “any type whose underlying type is int.” Without the ~, double(id) would fail to compile.
Generic structs
Type parameters work on structs too:
type Pair[T constraints.Ordered] struct {
First, Second T
}
func main() {
p1 := Pair[string]{"hello", "world"}
p2 := Pair[int]{1, 2}
fmt.Printf("%T\n", p1) // main.Pair[string]
fmt.Printf("%T\n", p2) // main.Pair[int]
}
One thing to be aware of: Pair[string] and Pair[int] are different types. You can’t assign one to the other or put them in the same slice.
The comparable constraint
comparable is a built-in constraint that matches any type that supports == and !=. It’s used throughout the standard library:
// slices.Contains — simplified
func Contains[S ~[]E, E comparable](s S, v E) bool {
for i := range s {
if s[i] == v {
return i
}
}
return false
}
The S ~[]E parameter means “any slice type whose element type is E”, which includes named slice types like type UserList []User.
One caveat: comparable does not imply ordered. Maps and structs can be comparable (you can check equality), but you can’t use < or > on them. If you need ordering, use constraints.Ordered.
Type inference
Go can usually infer the type parameter from the arguments you pass, so you don’t need to specify it explicitly:
add(1, 2) // T inferred as int
add(1.5, 2.5) // T inferred as float64
add("a", "b") // T inferred as string
You can always be explicit when inference isn’t possible or when it helps readability:
add[int](1, 2)
Gotchas
Methods can’t have type parameters. Type parameters belong on the type, not on individual methods. This won’t compile:
// invalid
func (s *MyStruct) Process[T any](v T) T { ... }
Instead, put the type parameter on the struct:
type MyStruct[T any] struct{}
func (s *MyStruct[T]) Process(v T) T { ... }
Interface types don’t satisfy union constraints. If your constraint is int | string, passing an interface{} that happens to hold an int won’t work. The type must be statically known at the call site.
Generics aren’t always the right tool. If your function only needs to call methods on a value, a regular interface is simpler and more idiomatic. Generics shine when you’re working with concrete types that share structure but not a method set — like numeric types or ordered values.
Conclusion
Generics give Go a way to write flexible, reusable code without sacrificing type safety or reaching for empty interfaces. The constraint system is the key — understand how type sets work and the rest follows naturally.
The standard library’s slices, maps, and cmp packages (added in Go 1.21) are good examples of generics used well. Worth reading through if you want to see the patterns in practice.