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:
- Write the same function ten times, once per type (
MaxInt,MaxFloat64, …). - 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.Tis a placeholder for whatever type the caller uses.any— the constraint. It means “literally any type.” (anyis just an alias forinterface{}.)v Tand*T—Tis 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 union — T 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
comparablebecause 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.Toor 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.