Why this post

You’ve learned Go. You can write a for loop, define a struct, maybe spin up an HTTP server. Then you open the Kubernetes source, see func ToPtr[T any](v T) *T, and your brain stalls.

This post is the bridge. By the end you’ll be able to read generic Go code in real codebases — Kubernetes especially — without flinching.

The 60-second mental model

Before generics (pre-Go 1.18), if you wanted a function that worked on any type, you had two bad options:

  1. Write the same function ten times, once per type (MaxInt, MaxFloat64, …).
  2. Use interface{} everywhere and lose all type safety.

Generics give you a third option: write the function once, but tell the compiler “this works for any type T that satisfies these rules.” The compiler then checks each call site for you.

That’s it. Generics are parameterized types — same idea as function parameters, but for types instead of values.

The syntax you need to recognize

Here’s a generic function:

func ToPtr[T any](v T) *T {
    return &v
}

Three new things compared to regular Go:

  • [T any] — the type parameter list. T is a placeholder for whatever type the caller uses.
  • any — the constraint. It means “literally any type.” (any is just an alias for interface{}.)
  • v T and *TT is now usable in the signature like any normal type.

Calling it:

p := ToPtr(42)          // T inferred as int → *int
s := ToPtr("hello")     // T inferred as string → *string

You usually don’t have to write ToPtr[int](42) — the compiler infers T from the argument.

Constraints: the gatekeeper

any is permissive. Often you want to say “T must support ==” or “T must be a number.” That’s what constraints are.

func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

comparable is a built-in constraint that means “supports == and !=.” Without it, the v == target line wouldn’t compile.

You can also define your own constraints as interfaces:

type Number interface {
    int | int64 | float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

The int | int64 | float64 is a type unionT must be one of these.

Generics in the wild: Kubernetes

Kubernetes adopted generics carefully, mostly in utility packages. Let’s read three real examples.

1. ptr.To — the pointer helper

From k8s.io/utils/ptr:

func To[T any](v T) *T {
    return &v
}

Before generics, Kubernetes had pointer.Int32(5), pointer.StringPtr("x"), pointer.BoolPtr(true) — a separate function per type. Now it’s one function. You’ll see this everywhere in Kubernetes:

replicas := ptr.To(int32(3))
enabled  := ptr.To(true)

2. sets.Set[T] — generic sets

From k8s.io/apimachinery/pkg/util/sets:

type Set[T comparable] map[T]Empty

func New[T comparable](items ...T) Set[T] {
    ss := Set[T]{}
    ss.Insert(items...)
    return ss
}

Two things to notice:

  • Set[T comparable] — a generic type. Same syntax as functions, but on a type definition.
  • The constraint is comparable because map keys in Go must be comparable.

In use:

nodes := sets.New[string]("node-1", "node-2", "node-3")
if nodes.Has("node-1") { /* ... */ }

3. slices.Contains — from the standard library, used heavily in k8s

func Contains[S ~[]E, E comparable](s S, v E) bool

This one has two type parameters. The ~[]E is a new piece of syntax — the ~ means “any type whose underlying type is []E.” That lets it work on named slice types too, like type NodeList []Node.

Don’t get hung up on ~ on your first read. Just know: it makes the function work on slice-like named types, not only raw []E.

When NOT to use generics

Beginners often go generics-crazy after learning them. Don’t. The Go team’s own advice:

  • If a regular function works, use a regular function.
  • If an interface works, use an interface.
  • Reach for generics when you’d otherwise be writing the same function multiple times for different types — like ptr.To or container types (sets, queues, trees).

A good smell test: if your generic function only ever has one call site, it probably shouldn’t be generic.

Wrap-up

You now have enough to read 90% of the generic code in Kubernetes:

  • [T any] — placeholder type, no restrictions
  • [T comparable] — supports ==
  • [T SomeInterface] — must satisfy that interface (possibly a type union)
  • ~[]E — any named type whose underlying type is []E

Next time you’re spelunking through k8s.io/utils or apimachinery, you’ll see these patterns repeat. The syntax is small. The hard part — knowing when to use it — comes later.

Open a Kubernetes file. Find a [T in the source. Read it. You’ll be fine.