---
title: "10 — Generics — Type Parameters, Constraints, and Inference"
uid: golang-generics
tags: ["type-parameters", "golang", "type-constraints", "type-inference", "generics", "roadmap:golang", "fmt-errorf"]
excerpt: "Generics add type parameters in square brackets, constrained by interfaces, inferred at the call site — one function, type-safe over many types, no empty-interface dance."
date: 2026-08-13T03:28:11+0000
source: https://www.aveshina.my.id/en/blog/golang-generics
---

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](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/](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](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](https://simonklee.dk/type-constraints)

[5] The Go Authors, "Type Inference," The Go Blog, 2024. [Online]. Available: [https://go.dev/blog/type-inference](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](https://go.dev/blog/error-handling-and-go)

```quiz
Q: Where are a function's type parameters declared?
- In angle brackets, like <T>
- In square brackets, like [T], before the regular parameters
correct: 1
explain: Go uses square brackets for type parameters: func Sum[T any](nums []T) T. Angle brackets were rejected to keep the grammar unambiguous with comparison operators.

Q: What does the ~ in a constraint like ~int64 mean?
- approximately an int64, accepting it loosely
- the underlying type is int64, so it also matches user-defined types like type UserID int64
correct: 1
explain: ~T matches any type whose underlying type is T. Without it, a constraint of int64 would reject type UserID int64; with it, the constraint accepts both.

Q: At the call site Sum([]int{1,2,3}), the type parameter T is…
- an error — you must always write Sum[int](...)
- inferred as int from the argument, so no explicit type argument is needed
correct: 1
explain: Type inference deduces T from the argument types when it can. You only need to write the explicit type argument when inference cannot determine it from context.

Q: What is the difference between fmt.Errorf with %w and with %v when wrapping an error?
- They are identical
- %w preserves the wrapped error so callers can use errors.Is/As; %v flattens it to a string and breaks the chain
correct: 1
explain: %w produces an error chain. errors.Is and errors.As can walk that chain. %v formats the inner error into the message text, losing the structured link to the original error.

Q: When should you reach for generics in Go?
- For every function, to maximize reuse
- When you are about to write the third copy of a function with only the type changed; otherwise concrete code reads better
correct: 1
explain: Generics add reading overhead. They pay off for typed containers, generic algorithms, and utilities that would otherwise be copy-pasted. Most domain logic is clearer with concrete types and interfaces.
```
