# Generics

Source: https://codewiki.com/go/generics/

> - **what**: Generics let functions and types declare type parameters, so one implementation can handle a set of concrete types while preserving static relationships between inputs and outputs.
> - **trap**: A constraint isn't a label; it determines which operations the compiler permits in generic code. `comparable` allows only `==` and `!=`, not `<`.
> - **fix**: Work backward from the operations the implementation needs to the smallest useful constraint. Put `~` before a type term when named types with the same underlying type should qualify.

## What it is and why it exists

A generic is a function or type with one or more type parameters. A concrete type replaces each parameter when you call a generic function or instantiate a generic type. The compiler still checks operations, arguments, and results at compile time, so callers don't have to assert a value from `any` back to the desired type.

Generics address code that repeats across types while keeping the same structure. Slice transformations, set lookups, and stacks care about relationships between element types, but shouldn't need separate implementations for `int`, `string`, and every domain type. Type parameters put that relationship in the signature: `Map[T, U any]([]T, func(T) U) []U` says that the converter accepts an element from the input and determines the result element type.

Interfaces and generics solve different problems. An interface value hides a dynamic concrete type and is useful when callers operate on different implementations through a method set; a generic preserves the concrete static type within each instantiation and suits algorithms and containers. If an implementation only calls a method such as `Read` or `String`, an ordinary interface parameter is often more direct. If the same type appears in several parameter or result positions, a type parameter usually expresses the contract better.

Go added generics in 1.18. You'll meet them in the standard library's `slices`, `maps`, and `cmp` packages, in reusable data structures, and in helpers that would otherwise require reflection. They aren't a way to remove all duplication: separate implementations are clearer when types require different semantics.

## How it works

### Type parameters preserve relationships

A type parameter appears in square brackets after a declaration's name. `T`, `K`, and `V` are type names used within that declaration, and every name has a constraint. Adjacent parameters with the same constraint can share it, as in `[T, U any]`.

Ordinary parameters and results of a generic function can use these names. Fields of a generic type can use them too, while methods redeclare the corresponding receiver type parameters in the receiver, as in `func (s *Stack[T]) Push(value T)`. This `T` corresponds to the parameter of `Stack`; it isn't a new type parameter introduced by the method.

Instantiation performs two checks. The compiler first confirms that every type argument satisfies its constraint, then creates the function or type instance for those arguments. The resulting `Stack[string]` and `Stack[int]` are different named types and aren't assignable to one another.

### Constraints determine available operations

A generic constraint is an interface. It describes the type set allowed to replace a type parameter and determines what the function body may do with that parameter. With an `any` constraint, code can rely only on behavior common to every type, such as assignment, passing, and returning values.

The predeclared `comparable` constraint accepts types that can be map keys and permits `==` and `!=`. It promises no ordering, so `a < b` doesn't compile. For ordered operations, use the standard library's `cmp.Ordered` or declare a constraint limited to the types your domain needs.

A constraint interface can embed methods, type terms, or both. `fmt.Stringer` requires a `String() string` method; `~int | ~int64` is a union accepting types whose underlying type is `int` or `int64`. Separate elements in a constraint intersect, so a type must meet both sides when you write a method and a type term.

### Type sets describe candidates

A type set is the set of all non-interface types represented by an interface. A method element keeps types implementing that method, a union combines candidate type terms, and separate embedded elements intersect. Generic code may use only operations supported with the same meaning by every type in the set.

The type term `int` means only the predeclared type `int`. `~int` also includes a named type such as `type Score int`, because its underlying type is `int`. The tilde isn't approximate runtime matching; it is constraint syntax checked statically by the compiler.

An interface containing type terms can be used only as a constraint, not as an ordinary variable type. You can write `func Max[T Ordered](...)`, but you can't declare `var value Ordered` to store any ordered value. If you need an interface value, use a basic interface containing only methods, or use `any` and validate the dynamic type at a boundary.

### Type inference removes call-site noise

Type inference lets the compiler derive omitted type arguments from function arguments and constraint relationships. In `Map(ids, strconv.Itoa)`, `ids` provides `T`, and the signature of `strconv.Itoa` provides `U`. Once inference succeeds, the caller doesn't need to write `Map[int, string]`.

Inference works for function calls and other specified contexts; it doesn't infer a generic type's arguments from composite-literal fields. You must write the `string` in `Stack[string]{}`. A function call can also lack enough information, such as when a type parameter occurs only in the result. In that case, supply a type argument explicitly or redesign the signature so an input carries the relationship.

Untyped constants participate in inference and representability checks. Mixing constant kinds can produce a wider default type than expected, or leave no type that satisfies both the constraint and arguments. API examples and tests should include variables, named types, and untyped constants instead of testing integer literals alone.

### Generic types still follow zero-value rules

Generic structs, defined slice types, and other named types follow ordinary Go rules after instantiation. A field's zero value comes from the actual type argument: `var item T` is `0` for `int` and `nil` for a pointer. When that zero value has domain meaning, returning only `T` can't report whether an operation succeeded.

Whether a generic container's own zero value is usable depends on its fields and methods. A stack backed by a slice can append immediately, so its zero value works well; a set backed by a map must initialize before its first write. A constructor, lazy initialization, or a documented zero-value contract can all work, but the API should choose one clear behavior.

## Examples

### Preserve input-output relationships with Map

The first example converts invoice numbers to labels. `T` and `U` can differ, and the call relies entirely on inference. The return type is still `[]string`, with no assertion required.

<!-- quick -->

```go
package main

import (
	"fmt"
	"strconv"
)

func Map[T, U any](values []T, convert func(T) U) []U {
	result := make([]U, len(values))
	for i, value := range values {
		result[i] = convert(value)
	}
	return result
}

func main() {
	invoiceIDs := []int{7, 21, 42}
	labels := Map(invoiceIDs, func(id int) string {
		return "INV-" + strconv.Itoa(id)
	})

	fmt.Println(labels)
}
```

```text
[INV-7 INV-21 INV-42]
```

<!-- /quick -->

`Map` only allocates a result, reads `T`, calls the converter, and writes `U`, so `any` is sufficient. Adding a numeric union wouldn't enable another operation; it would only reject strings and structs. This signature is useful because it preserves a relationship, not because it loosens runtime types.

### Accept named types with a type set

This maximum function needs `>`, so it declares a type set that actually supports ordering. `Score` is a distinct named type whose underlying type is `int`; `~int` lets it satisfy the constraint. An extra Boolean distinguishes empty input from a valid zero value.

```go
package main

import "fmt"

type Ordered interface {
	~int | ~int64 | ~float64 | ~string
}

type Score int

func Max[T Ordered](values []T) (T, bool) {
	if len(values) == 0 {
		var zero T
		return zero, false
	}

	best := values[0]
	for _, value := range values[1:] {
		if value > best {
			best = value
		}
	}
	return best, true
}

func main() {
	best, ok := Max([]Score{72, 91, 84})
	fmt.Println(best, ok)

	empty, ok := Max([]Score(nil))
	fmt.Println(empty, ok)
}
```

```text
91 true
0 false
```

If `~int` becomes `int`, `Score` no longer satisfies the constraint. The function doesn't need to know whether the argument is specifically `Score`; the constraint has already proved that `>` is valid for every candidate. The returned `best` retains the `Score` type.

### Make a generic container's zero value useful

The underlying representation of `Stack[T]` is a slice. A nil slice can be appended to, so callers don't need a constructor. `Pop` returns `(T, bool)` to distinguish an empty stack from a stored zero value.

```go
package main

import "fmt"

type Stack[T any] []T

func (s *Stack[T]) Push(value T) {
	*s = append(*s, value)
}

func (s *Stack[T]) Pop() (T, bool) {
	if len(*s) == 0 {
		var zero T
		return zero, false
	}

	last := len(*s) - 1
	value := (*s)[last]
	*s = (*s)[:last]
	return value, true
}

func main() {
	var stages Stack[string]
	stages.Push("review")
	stages.Push("publish")

	for range 3 {
		stage, ok := stages.Pop()
		fmt.Printf("%q %t\n", stage, ok)
	}
}
```

```text
"publish" true
"review" true
"" false
```

The receiver is `*Stack[T]` because both `Push` and `Pop` replace the slice descriptor. The element type still comes from the `string` instantiation. If storage changes to a map, writes through the zero value stop being safe, so the API contract must change too.

## Pitfalls

### Treating `comparable` as ordered

> **Pitfall:** `comparable` guarantees only `==` and `!=`. Generated code declaring `func Min[T comparable](a, b T)` and then writing `a < b` in the body won't compile.

Slices, maps, and functions don't satisfy `comparable`; most scalars, pointers, channels, interfaces, and arrays or structs with comparable fields do. Comparability still doesn't imply order. Booleans and structs are immediate counterexamples.

**Fix:** use the standard library's `cmp.Ordered`, or declare a constraint containing only the predeclared and named types you need. When ordering is a domain rule, accept a comparison function instead of assuming `<` expresses that rule.

### Performing extra operations under `any`

> **Pitfall:** `[T any]` doesn't make `+`, field selection, indexing, or method calls available. The body can perform only operations guaranteed across the constraint's whole type set.

Changing the code to a type switch usually works around the static relationship and misses named types. It also turns every added type branch into runtime maintenance.

**Fix:** derive the constraint from operations in the body. Embed a method interface when you need a method, declare suitable type terms when you need an operator, and keep `any` when the implementation only moves values around.

### Forgetting `~` and rejecting domain types

> **Pitfall:** The constraint `int | string` includes only those two predeclared types. `type UserID int` doesn't satisfy it merely because it can be converted to `int`.

Forcing callers to convert a `UserID` to `int` discards useful static information and can mix identifiers from different domains. Constraints describe type identity and underlying types, not general conversion rules.

**Fix:** use `~int` if the algorithm is valid for every named type with that underlying type. If the API should accept only the exact predeclared type, retain `int` and document and test that boundary.

### Adding type parameters to a method

> **Pitfall:** Go methods can't declare new type parameters beyond the receiver's parameters. `func (s Stack[T]) Map[U any](...)` is a syntax error.

Adding every future parameter to the receiver in advance makes each instantiation carry unrelated types. It also promotes a relationship used by one operation into the identity of the whole type.

**Fix:** write a standalone function such as `MapStack[T, U any](Stack[T], func(T) U) Stack[U]` when the operation introduces `U`. Put a parameter on the generic type only when it determines fields or the long-term contract of its methods.

### Using a zero value to mean failure

> **Pitfall:** A `First`, `Min`, or `Pop` returning only `T` can't distinguish failure from a valid zero value. The value is `""` for strings, `nil` for pointers, and a struct with every field zeroed for structs.

Generated code often introduces `var zero T` to make a return compile but never decides how the caller should interpret it. The problem isn't unique to generics, but a type parameter means the zero value's shape isn't known while writing the function.

**Fix:** return `(T, bool)` or `(T, error)` and make the caller handle absence explicitly. Return a lone `T` only when the domain contract makes its zero value the one correct result.

<!-- deep -->

## How type sets combine

### Interface elements intersect

A constraint interface starts from the set of all non-interface types, then each element narrows it. A method element keeps types whose method set contains that method, a single type term keeps its corresponding types, and an embedded interface contributes its type set. To satisfy the complete constraint, a type must remain in every set produced by its elements.

This explains why writing both `~int` and `String() string` isn't an either-or choice. A candidate must have `int` as its underlying type and declare the required method. The predeclared `int` type itself has no such method, while a named integer type that declares one can satisfy the intersection.

### Unions combine candidate terms

The vertical bar `|` means union only within one union element. `~int | ~int64 | ~float64` accepts any of those terms, but the constraint's other elements still apply. Non-interface terms in a multi-term union must be pairwise disjoint, so `int | ~int` is invalid because the former is already contained in the latter.

A union is a closed list. Choosing one means a future numeric type isn't accepted automatically, which can be exactly the stable boundary an API needs. If the algorithm depends on behavior rather than predeclared representation, a comparison function or method constraint may be easier to extend.

### `~` checks underlying types

In an approximation element `~T`, `T` must be its own underlying type and can't be a type parameter. Given `type MyInt int`, `~MyInt` is invalid because the underlying type of `MyInt` isn't itself; write `~int`. The compiler then includes every named type with that underlying type.

Sharing an underlying type doesn't make two named types directly assignable to each other. The constraint decides whether a type can be an argument and which operations the body may use. After instantiation, parameters and results still preserve the named type supplied by the caller.

## Inference and instantiation boundaries

### Function arguments provide equations

The compiler unifies occurrences of type parameters in function parameters with argument types. If a parameter is `[]T` and its argument is `[]Invoice`, it can derive `T` as `Invoice`. If the same parameter occurs in several positions, those positions must provide compatible answers; inference fails instead of picking a common `any`.

Constraints can provide another relationship. A declaration shaped like `[S ~[]E, E any]` can first derive `S` from the argument and then infer `E` from the underlying slice type of `S`. Generic slice functions in the standard library often use this shape to preserve a named slice type while still knowing its element type.

### Result context isn't a general inference source

Don't assume an assignment target can always determine a function's type arguments backward. The most reliable public APIs let ordinary arguments carry the information required for inference. If a constructor only returns `T` and receives no value related to `T`, callers generally need an explicit type argument.

A partial type-argument list can provide leading parameters and let the compiler infer the rest, but that makes parameter order part of usability. Put the parameter callers are most likely to specify first. Testing real calls is more reliable than judging how tidy the declaration looks.

### Generic types require explicit instantiation

Using a generic type requires instantiation; field values don't fill in type arguments. `Pair[int, string]{...}`, `var stack Stack[Task]`, and ordinary assignment involving an already instantiated value are valid. Writing only `Stack{}` isn't contextual shorthand; it omits the type argument.

An instantiated type continues to follow ordinary assignability and method-set rules. Methods with receiver `*Stack[T]` belong to the corresponding pointer type's method set; generics don't change the distinction between value and pointer receivers. When checking interface implementation, inspect `Container[T]` and `*Container[T]` separately.

## Generic API tradeoffs

### Prefer parameters that express relationships

Useful type parameters usually occur more than once in a signature. A parameter may connect input elements to callback arguments, connect a map's key type to a lookup key, or make container methods return the same element type. A parameter that occurs once with an `any` constraint often expresses no relationship, and an ordinary parameter or interface may be simpler.

Don't introduce generics merely to avoid one type assertion. Interfaces suit heterogeneous values and runtime dispatch; type parameters suit consistent types within one instantiation. The choice depends on what information callers must retain, not on which syntax is newer.

### Keep constraints minimal and meaningful

A constraint that's too broad leaves the body unable to express its algorithm, while one that's too narrow rejects valid callers. The minimal constraint is the one that proves exactly the operations the implementation needs, not the one with the fewest written terms. A `[T any]` sorter accepting a comparison function may be more general than an enormous `Ordered` union because the caller supplies the order.

An exported constraint is API. Adding candidate types usually preserves existing calls, but changing available operations, removing a term, or changing method requirements can break users. If a constraint serves one function and has no reuse value, keeping it unexported reduces the promised surface.

### Define a clear zero-value contract

A generic function can't assume the zero value of `T` is distinguishable from domain absence. Container lookups commonly return `(V, bool)`, while parsing and external operations commonly return `(T, error)`. Those shapes follow map and standard-library conventions and make generated code straightforward to test.

The zero value of the container itself is a separate decision. Slice-backed storage often supports it naturally; maps, channels, and structures requiring a configured capacity usually need an initialization policy. Documentation should distinguish the result contract for a missing element from behavior of an uninitialized container.

### Don't claim performance without measurements

Generic code generation is a compiler implementation detail, not a fixed performance guarantee in the language specification. Different type arguments, method calls, escape behavior, and compiler versions can produce different results, so syntax alone can't prove zero cost or a degree of code growth.

If performance determines an API choice, benchmark the target Go version with real type arguments and workload, then inspect escape analysis and generated code. Without that evidence, discuss type safety, readability, and maintenance cost instead; those properties are directly reviewable from the source.

## Verify generic boundaries

A generic implementation can look correct for one common type and still fail at a constraint boundary. Verification should start from the promised type set, not from the `int` that current callers happen to use. Compile-time and runtime tests catch different failures, so you need both.

### Use compile cases to check constraints

Write a minimal instantiation for every type category the API should support and keep those cases compiling in CI. Include a predeclared type, a named type with the same underlying type, and a type the constraint deliberately rejects. Put the last category in a dedicated compile-failure or analysis-tool test rather than leaving source that breaks the ordinary suite.

Successful compilation proves only that a candidate satisfies the constraint, not that the algorithm returns the right result. `Max([]Score{...})` still needs cases for order, duplicates, and empty input. Conversely, runtime tests using only `int` won't reveal that a constraint incorrectly excludes `Score`.

### Check inference at call sites

Tests should use the call form the documentation intends to show. If an example omits type arguments, compile that exact call rather than writing the full `[int, string]` in tests and claiming inference works. Function values, nil arguments, and untyped constants are particularly likely to change an inference result.

Recompile representative calls after reordering parameters or moving a value into the result. The function body may be unchanged while information required for inference has disappeared. API usability lives at the call site, not only in the declaration.

### Exercise zero values at runtime

Test every failure path returning `T` with the valid zero value as successful data. For `Stack[string]`, actually push `""`; for `Stack[*Task]`, decide whether a nil pointer is valid domain data. Only then can a test prove that callers handle the extra Boolean or error correctly.

Start container tests from an unconstructed zero value too. A slice implementation may work while a map implementation panics on a write. This test prevents an internal representation change from silently breaking a public zero-value contract.

### Tools don't replace contract review

`go test` runs behavioral checks and compiles reached instantiations, while `go vet` checks a selection of suspicious constructs. Neither tool decides whether a constraint is narrower than the business contract or whether a type parameter expresses no relationship. Those questions still require a signature-level review.

A test matrix doesn't have to enumerate every member of a type set. Pick representatives that distinguish rules: exact and approximate terms, zero and nonzero values, and inferred and explicit calls. Each case should test a way the declaration could fail rather than repeat the same evidence for quantity.

### Verify package boundaries

Compile at least one call from a consumer package because exported names, inference, and unexported constraints are fully exercised only across that boundary. In-package tests can miss API problems by seeing extra names.

Keep each call case small and make its failure point to one contract. These are interface compatibility tests, not another algorithm suite.

<!-- /deep -->

[Checkpoint: go/generics](https://codewiki.com/go/generics/#checkpoint)

## Further reading

- [Go language specification: type parameter declarations](https://go.dev/ref/spec#Type_parameter_declarations)
- [Go tutorial: getting started with generics](https://go.dev/doc/tutorial/generics)
- [Go blog: an introduction to generics](https://go.dev/blog/intro-generics)
- [Standard library documentation for `cmp.Ordered`](https://pkg.go.dev/cmp#Ordered)
