Go Advanced #3 Generics — Type Parameters and Constraints
After #2 sync Package, this time a tool of a different flavor. Generics.
Introduced in Go 1.18 (2022). Patterns that previously required interface{} (now any) and type assertions can now be expressed in a type-safe way.
Where generics fit #
func Min[T int | float64 | string](a, b T) T {
if a < b {
return a
}
return b
}
Min(1, 2) // 1
Min(1.5, 2.5) // 1.5
Min("a", "b") // "a"[T int | float64 | string] is the type parameter. T is determined at call time.
Two key parts #
- type parameter list —
[T constraint, U constraint, ...]after the function name - constraint — what T must satisfy. Expressed via interfaces
Constraint — interfaces, extended #
Existing interfaces only expressed a method set. With generics, an interface can also express a type set.
type Number interface {
int | float64
}
func Sum[T Number](nums []T) T {
var sum T
for _, n := range nums {
sum += n
}
return sum
}The Number interface is satisfied only by types that are int or float64. It works as a constraint even with no methods.
~ token — include underlying-type-based types
#
type Number interface {
~int | ~float64
}
type Celsius float64
Sum([]Celsius{1, 2, 3}) // OK — Celsius's underlying type is float64~int — every type whose underlying type is int. Includes named types like type ID int.
Without ~, plain int | float64 — defined types like Celsius aren’t usable.
comparable — types that compare with == and !=
#
The set of types that support == !=. Appears in places like map keys.
func Index[T comparable](s []T, target T) int {
for i, v := range s {
if v == target {
return i
}
}
return -1
}comparable is a predefined constraint that includes int, string, and structs (when all fields are comparable), but slices, maps, and functions are excluded because they do not support ==.
any — no constraint
#
func First[T any](s []T) T {
return s[0]
}any is an alias for interface{} and accepts any type. It is useful only when you perform almost no operations on the type — just indexing or assignment.
golang.org/x/exp/constraints — standard constraints
#
Common constraints are predefined in a separate package.
import "golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}constraints.Ordered — types that support < > <= >= (numbers + string).
Generic data structures #
The most natural fit. Data structures usually behave the same regardless of the contained type.
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}
func main() {
s := Stack[int]{}
s.Push(1)
s.Push(2)
v, _ := s.Pop() // 2
_ = v
}var zero T — the standard pattern for getting a generic type’s zero value.
Type inference for generic functions #
Sum([]int{1, 2, 3}) // T inferred as int
Sum[int]([]int{1, 2, 3}) // explicit
stack := Stack[int]{} // type argument explicitFunction calls — auto-inferred from argument types. Data structures or method calls — often need explicit type arguments.
Standard library use — slices, maps
#
In Go 1.21, generic-based standard packages were added.
import "slices"
slices.Contains([]int{1, 2, 3}, 2) // true
slices.Sort([]string{"b", "a", "c"}) // sort
slices.Index([]int{1, 2, 3}, 2) // 1
slices.Reverse([]int{1, 2, 3}) // [3 2 1]
import "maps"
maps.Keys(m) // all keys of a mapHelpers you used to write by hand — now in the standard library.
Pitfall — methods can’t introduce type parameters #
type Box[T any] struct{ v T }
// methods can't define new type parameters
func (b Box[T]) Map[U any](f func(T) U) Box[U] { // ✗ compile error
// ...
}Methods can only use the receiver’s type parameter (T) — they cannot introduce a new type parameter. For such transformations, write a function instead.
func MapBox[T, U any](b Box[T], f func(T) U) Box[U] {
return Box[U]{v: f(b.v)}
}Pitfall — overly broad constraint #
func Process[T any](v T) {
// any — almost nothing you can do with v
}The wider the constraint, the fewer operations you can apply to T. Use Number for arithmetic, comparable for equality, Ordered for ordering — only as much as the operation requires.
Pitfall — sometimes interfaces are still better #
// generics
func Print[T fmt.Stringer](v T) {
fmt.Println(v.String())
}
// interface
func Print(v fmt.Stringer) {
fmt.Println(v.String())
}In this case, the interface is more natural. When you need dynamic dispatch — different types mixed in one slice, for example — use interfaces, not generics.
Simple guideline #
- Data structures where type safety and performance matter → generics
- Polymorphism is essential (mixing different types in a container) → interface
- Want one function to accept multiple types → either; decide on readability
Pattern — Map / Filter / Reduce #
Go intentionally didn’t include them in the standard library, but generics make them natural.
func Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, len(s))
for i, v := range s {
out[i] = f(v)
}
return out
}
func Filter[T any](s []T, pred func(T) bool) []T {
var out []T
for _, v := range s {
if pred(v) {
out = append(out, v)
}
}
return out
}
func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
acc := init
for _, v := range s {
acc = f(acc, v)
}
return acc
}If you use these often, gather them in a helper package. That said — Go’s general style is more often to write the for loop explicitly (it’s seen as more readable).
How far to abstract #
Just because generics exist does not mean every piece of code should become generic. Consider abstracting once you have seen the same shape three or more times — no earlier.
“Don’t be afraid to write a little code.” — Rob Pike
Just as Go intentionally keeps things simple, approach generics in the same spirit. Generalizing where it is not needed often hurts readability.
Wrap-up #
What we covered:
- type parameter —
[T constraint]after the function/type name - constraint — interface that expresses method set + type set
~T— includes types based on the underlying typecomparable— supports==comparisonany— no constraint (=interface{})constraints.Ordered— orderable types- Data structures — where generics fit best
- Methods can’t introduce new type parameters
- When polymorphism is essential — interfaces are natural
- Abstract when the same shape appears three or more times
In the next post (#4 reflect Package) — reflect for handling types at runtime. We’ll cover the tool you see inside serialization libraries.