Skip to content
C Codeloom
Go

Go Generics Deep Dive

How Go's generics work in practice: type parameters, constraints, the constraints package, and where generics shine versus interfaces or code generation.

·4 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How type parameters are declared in Go
  • Writing and reading constraint interfaces
  • The role of comparable and ordered constraints
  • When generics are clearer than interfaces
  • Performance characteristics of generic code

Prerequisites

  • Comfort with Go interfaces and basic syntax

What and Why

Generics arrived in Go 1.18 after years of debate. They let you write functions and types that work uniformly across many element types without losing static type safety. Before generics, you would either duplicate code, use interface{} with type assertions, or generate code at build time. None of those were great.

Generics are intentionally less expressive than templates in C++ or generics in Java. They prioritize predictable behavior, fast compile times, and code that still reads like Go.

Mental Model

A type parameter is declared in square brackets after the function or type name, with a constraint that says what operations are valid.

func Map[T any, U any](xs []T, f func(T) U) []U
       ^^^^^^^^^^^^^^
       type parameters with constraints
                     (any = empty interface)

func Sum[T int | float64](xs []T) T
       ^^^^^^^^^^^^^^^^^^^^^^^
       constraint is a union type:
       T must be int OR float64
Generic function anatomy

A constraint is just an interface, possibly with a type set. The any keyword is an alias for interface{}. The comparable constraint is built-in and means == and != work on the type.

Hands-on Example

A small generic toolkit.

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

func Map[T any, U any](xs []T, f func(T) U) []U {
	out := make([]U, len(xs))
	for i, x := range xs {
		out[i] = f(x)
	}
	return out
}

func Filter[T any](xs []T, pred func(T) bool) []T {
	out := xs[:0:0]
	for _, x := range xs {
		if pred(x) {
			out = append(out, x)
		}
	}
	return out
}

func Sum[T constraints.Ordered](xs []T) T {
	var s T
	for _, x := range xs { s += x }
	return s
}

// A small generic Set type
type Set[T comparable] map[T]struct{}

func (s Set[T]) Add(v T)      { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }

func main() {
	nums := []int{1, 2, 3, 4, 5}
	squared := Map(nums, func(n int) int { return n * n })
	evens := Filter(nums, func(n int) bool { return n%2 == 0 })
	fmt.Println(squared, evens, Sum(nums))

	s := Set[string]{}
	s.Add("ada"); s.Add("bob")
	fmt.Println(s.Has("ada"))
}

Three observations. Type inference handles most calls; you almost never need to spell out the type arguments. The Set type is a generic map alias with methods. constraints.Ordered is a constraint from the golang.org/x/exp/constraints package that includes integers, floats, and strings (anything < works on).

Common Pitfalls

  • Over-genericizing: not every helper should be generic. func sum(xs []int) int is shorter and clearer than func Sum[T constraints.Integer](xs []T) T when you only ever pass []int.
  • Methods cannot add type parameters: you can have generic types with methods, but the methods cannot introduce new type parameters. Move that logic to a top-level function.
  • any defeats inference: declaring slices as []any forces callers to box every element, losing the benefit of generics. Use specific types or type parameters.
  • Comparison vs ordering: comparable allows ==, not <. Use the constraints.Ordered constraint when you need ordering.
  • Reflection still exists: do not assume generics replace reflection. Some patterns (deep equality, encoding) still need it.

Practical Tips

Use generics when the algorithm is the same across types. Container types (sets, queues, ring buffers), slice utilities, and channel helpers are great fits.

Use interfaces when you care about behavior (io.Reader, http.Handler) rather than the shape of values. Interfaces remain the right tool for plug-in points.

When writing a constraint, start with the smallest type set that lets the body compile. Wider constraints make APIs more flexible; narrower ones surface compiler errors sooner.

Performance-wise, the Go compiler currently uses a hybrid of monomorphization and dictionary passing. The cost depends on the constraint, but for typical generic helpers it is close to hand-written code. Benchmark before assuming overhead.

For library authors, expose generic functions sparingly in public APIs. Each one becomes part of your stability contract. Internal helpers are a great place to experiment.

Wrap-up

Go’s generics are deliberately modest, and that is a feature. They give you parametric polymorphism for the obvious cases (containers, slice helpers, numeric routines) without inventing a new language. Reach for them when the same algorithm applies to many types, prefer interfaces when behavior is the abstraction, and let type inference keep call sites clean. Used with restraint, generics quietly remove a layer of boilerplate without changing how Go feels.