AV
HomeAboutProjectBlog

© 2026 Ave syah Shina. All rights reserved.

  1. Home
  2. Blog
  3. 10 — Generics — Type Parameters, Constraints, and Inference

10 — Generics — Type Parameters, Constraints, and Inference

August 13, 20266 min read
Download as Markdown

Seventeen years without generics, and the feature finally landed in 1.18 (2022) — the wait made me curious about what it actually buys. The model that clicked: generics let you write one function or type that works over a type parameter T, declared in square brackets and constrained by an interface; the compiler infers T at the call site, so the result is type-safe and reusable without the empty-interface dance. [1] Generics did not replace interface{} everywhere — most Go code still reads as if generics do not exist — but for a specific class of repetitive utility code (containers, algorithms, helpers), they removed the duplication that used to force a choice between copy-paste and losing your types.

Why generics — the problem they solve

Before 1.18, the moment you wanted a function that worked on "a slice of anything," you had three bad options [2]:

  • Copy-paste a SumInts, SumFloats, SumInt64s — type-safe but duplicated.
  • Use interface{} — one function, but you lose all type information, pay for boxing (each value is wrapped in a generic container at runtime), and must type-assert at runtime.
  • Run a code generator — generates the copies from a template, adding a build step.

Generics add a fourth: write the function once with a type parameter, and the compiler produces a type-safe version for each concrete type used. No duplication, no lost types, no build step.

Generic functions — the syntax

A generic function declares its type parameters in square brackets before the regular parameters [3]:

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

Sum([]int{1, 2, 3}) // returns int, 6
Sum([]float64{1.5, 2.5}) // returns float64, 4.0

The [T int | float64] says: T is a type parameter that may be either int or float64. The function body uses T as if it were a real type, and the compiler guarantees that the slice's element type matches the return type. Notice the call sites do not specify T — that is type inference doing its job [5].

Constraints — the type parameter's contract

The int | float64 form is an inline constraint, but the real power is named constraints defined as interfaces [4]. The standard library's cmp package and golang.org/x/exp/constraints ship the common ones, and you write your own:

// A constraint: T must be ordered (supports <, >, <=, >=)
type Number interface {
~int | ~int64 | ~float64
}

func Max[T Number](a, b T) T {
if a > b {
return a
}
return b
}

The ~ means "and any type whose underlying type is this" — so ~int64 matches both int64 and type UserID int64. Built-in constraints include any (an alias for interface{}, no restriction) and comparable (supports == and !=, required for map keys). A constraint is just an interface; the only new ingredient is that interfaces can now also carry a _type set_ (the | list of allowed underlying types), not just methods.

Generic types

Entire types can be generic too — containers being the obvious case [3]:

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) {
var zero T
if len(s.items) == 0 { return zero, false }
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}

A Stack[int] and Stack[string] are distinct, fully type-safe types with no boxing. The method receivers carry the type parameter [T] so the compiler knows Push and Pop belong to the parameterized Stack.

Type inference — and when it does not fire

At the call site, Go infers T from the argument types whenever it can [5]. Sum([]int{...}) infers T = int. Inference fails when there is not enough information — a call like reflect.TypeOf(0) works, but some generic helpers need an explicit instantiation: Sum[int](nums). The general rule: if your function's arguments give the compiler enough to pin down every type parameter, you omit the brackets; otherwise you supply them. Inference is purely a call-site convenience and never changes the generated code.

A related essential — fmt.Errorf and error wrapping

Sitting alongside generics (because both arrived as the language matured) is the %w verb in fmt.Errorf, added in Go 1.13 [6]. It wraps an existing error while preserving it, producing an error chain:

if err := readConfig(); err != nil {
return fmt.Errorf("startup failed: %w", err)
}

The caller can then inspect the chain with errors.Is (does the chain contain a specific sentinel?) and errors.As (does it contain a value of a specific type?). This is the modern idiomatic way to add context to an error without losing the original. The older %v verb would format the inner error as a string, breaking the chain; %w keeps it intact. The choice between %v and %w is whether the caller should be able to inspect the wrapped error.

How I use this

Generics earn their keep in a narrow but real set of places: typed containers (Stack, Set, Result), generic algorithms (Map, Filter, Reduce-style helpers over slices), and any utility that was previously copy-pasted per type. For business logic, plain concrete types and interfaces are still clearer — generics add reading overhead, and most domain code does not repeat across types. The rule I use: reach for generics when I am about to write the third copy of a function with only the type changed; until then, a concrete function reads better. And %w is the default every time I wrap an error — preserving the chain costs nothing and lets callers make decisions with errors.Is/errors.As.

References

[1] The Go Authors, "Tutorial: Getting started with generics," Go Documentation, 2024. [Online]. Available: https://go.dev/doc/tutorial/generics

[2] LogRocket, "Understanding Generics in Go 1.18," 2022. [Online]. Available: https://blog.logrocket.com/understanding-generics-go-1-18/

[3] The Go Authors, "Generic Functions," Go Documentation, 2024. [Online]. Available: https://go.dev/doc/tutorial/generics

[4] S. Klee, "A walkthrough of type constraints in Go," simonklee.dk, 2022. [Online]. Available: https://simonklee.dk/type-constraints

[5] The Go Authors, "Type Inference," The Go Blog, 2024. [Online]. Available: https://go.dev/blog/type-inference

[6] The Go Authors, "Error handling and Go," The Go Blog, 2011. [Online]. Available: https://go.dev/blog/error-handling-and-go

Knowledge check · Question 1 of 5

Where are a function's type parameters declared?

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!