# Go Generics

> Guides Go generics restraint and constraint design — reach for a type parameter only when you would otherwise write the exact same code for multiple types (a container, or an algorithm over a slice/map/channel element type like map/filter/reduce), never when you only call a method on the value (that is an interface), and prefer the stdlib slices/maps/cmp helpers over hand-rolled generics. Covers constraints (any, comparable, cmp.Ordered, the ~T underlying-type element, unions like int | int64), type inference, generic type aliases (1.24), and self-referential constraints (1.26). Auto-invokes when writing or editing type parameters ([T any]), type constraints, generic functions or types, and on "should this be generic", "generic or interface here", or "make this generic". A type parameter earns its place only on real duplication; clarity first.

- Skill: `ctoth/go-generics` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add ctoth/go-generics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ctoth/go-generics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: ctoth (https://skillmd.com/u/ctoth)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ctoth/go-generics

---


# Go Generics

> "If you find yourself writing the exact same code multiple times, where the only difference between the copies is that the code uses different types, consider whether you can use a type parameter."
> — [When To Use Generics](https://go.dev/blog/when-generics)

> "If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter."
> — [When To Use Generics](https://go.dev/blog/when-generics)

> "Write Go programs by writing code, not by defining types. ... if you start writing your program by defining type parameter constraints, you are probably on the wrong path. Start by writing functions."
> — [When To Use Generics](https://go.dev/blog/when-generics)

Type parameters ([added in Go 1.18](https://go.dev/blog/intro-generics)) let one function or type work over a *set* of types. They are powerful and easy to over-reach for. This skill is mostly about *restraint*: a type parameter earns its place only when the alternative is writing the same code for several types. When all you do is call a method, the right tool is an interface, not a generic — that boundary is the central decision here.

---

## 1. The Headline Rule: Don't Reach for Generics First

The default is *no* type parameter. Write the concrete function. Generics come later, and only on proven duplication: "you should avoid type parameters until you notice that you are about to write the exact same code multiple times" ([When To Use Generics](https://go.dev/blog/when-generics)). The policy root states this as axis 2 — over-abstraction — and routes the depth here ([`go-idiomatic-discipline`](https://go-proverbs.github.io/): "Clear is better than clever").

```go
// WRONG — a type parameter with one concrete caller, reachable for "flexibility"
func First[T any](s []T) T { return s[0] }
func firstUser(us []User) User { return First(us) } // the only call site

// RIGHT — write the concrete function; add the type parameter when a SECOND
// element type actually needs the identical code.
func firstUser(us []User) User { return us[0] }
```

The starting guideline is blunt: "write Go programs by writing code, not by defining types ... Start by writing functions. It's easy to add type parameters later when it's clear that they will be useful" ([When To Use Generics](https://go.dev/blog/when-generics)).

---

## 2. The Rules and Their Sources

| Rule | The discipline | Source |
|---|---|---|
| Don't reach first | No type parameter until you'd write the same code for multiple types | "avoid type parameters until you notice that you are about to write the exact same code multiple times" ([When Generics](https://go.dev/blog/when-generics)) |
| Method call → interface | If you only call a method on the value, use an interface | "If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter" ([When Generics](https://go.dev/blog/when-generics)) |
| Don't swap interfaces for type params | A working interface signature stays an interface | "Omitting the type parameter makes the function easier to write, easier to read, and the execution time will likely be the same" ([When Generics](https://go.dev/blog/when-generics)) |
| Same impl → type param; different impl → interface | Decide by whether the method body is identical across types | "if the implementation of a method is the same for all types, use a type parameter ... if the implementation is different for each type, then use an interface type" ([When Generics](https://go.dev/blog/when-generics)) |
| Element-type functions | Generic fits functions over slice/map/channel elements that assume nothing about the element | "writing functions that operate on the special container types ... slices, maps, and channels ... the function code doesn't make any particular assumptions about the element types" ([When Generics](https://go.dev/blog/when-generics)) |
| Prefer the stdlib | Use `slices`/`maps`/`cmp` before rolling your own | `slices.Contains`, `slices.Sort`, `maps.Keys` ([pkg.go.dev/slices](https://pkg.go.dev/slices)) |
| Constraints are interfaces | A constraint is an interface defining the permitted type set | "In Go, type constraints must be interfaces" ([Intro Generics](https://go.dev/blog/intro-generics)) |
| `~T` for named types | Use `~T` so types *whose underlying type is* `T` qualify | "The expression `~string` means the set of all types whose underlying type is `string`" ([Intro Generics](https://go.dev/blog/intro-generics)) |

---

## 3. Generic or Interface? The Central Boundary

This is the decision that goes wrong most often, and the rule is mechanical. Look at what the body *does* with the value:

- It only **calls a method** on the value → use an **interface**. "If all you need to do with a value of some type is call a method on that value, use an interface type, not a type parameter" ([When To Use Generics](https://go.dev/blog/when-generics)).
- The method body would be **identical for every type**, and the code operates on the *data* (indexing a slice, comparing with `<`, using as a map key) → use a **type parameter**. "if the implementation of a method is the same for all types, use a type parameter. Inversely, if the implementation is different for each type, then use an interface type and write different method implementations, don't use a type parameter" ([When To Use Generics](https://go.dev/blog/when-generics)).

```go
// WRONG — type parameter, but the body only calls String(); this is interface work
func describe[T fmt.Stringer](v T) string { return "value: " + v.String() }

// RIGHT — an interface says exactly this and reads simpler
func describe(v fmt.Stringer) string { return "value: " + v.String() }
```

And the explicit prohibition, so the model never "modernizes" a fine interface signature into a generic one: "do not replace interface types with type parameters" — "Don't make that kind of change" ([When To Use Generics](https://go.dev/blog/when-generics)). The reciprocal rule lives in **[`go-interfaces`](../go-interfaces/SKILL.md)** §5: `any` says nothing, and behavior is named with an interface, not a type parameter.

---

## 4. Constraints: `any`, `comparable`, `cmp.Ordered`, `~T`, Unions

A constraint *is an interface* — "type constraints must be interfaces" ([Intro Generics](https://go.dev/blog/intro-generics)) — and it controls which operations the body may use. Pick the *narrowest* one that the code needs:

- **`any`** — no operations beyond assignment and passing around (the element of a container you only store and retrieve).
- **`comparable`** — allows `==` / `!=` and use as a map key. It "denotes the set of all non-interface types that are strictly comparable" ([Spec — Type constraints](https://go.dev/ref/spec#Type_constraints)). Do not use `any` and then `==`; that does not compile.
- **`cmp.Ordered`** — allows `< <= >= >`. It "permits any ordered type: any type that supports the operators `< <= >= >`" ([cmp](https://pkg.go.dev/cmp)). Prefer it over hand-writing the union.
- **`~T` (approximation element)** — `~int` is "the set of all types whose underlying type is" `int` ([Intro Generics](https://go.dev/blog/intro-generics)). Without the tilde, a named type like `type Celsius float64` would *not* satisfy a plain `float64` term. The standard library uses `~` everywhere (`cmp.Ordered` is all `~` terms), and you should too.
- **Union elements** — `int | int64` is the union of the listed type sets ([Spec — Type constraints](https://go.dev/ref/spec#Type_constraints)). Combine with `~` for named types: `~int | ~int64`.

```go
// A constraint that admits named types built on the listed kinds, and allows <:
type Number interface {
	~int | ~int64 | ~float64
}

func Max[T cmp.Ordered](a, b T) T { // cmp.Ordered, not a hand-rolled union
	if a > b {
		return a
	}
	return b
}
```

`comparable` and constraints embedding it "may only be used as type constraints. They cannot be the types of values or variables" ([Spec — Type constraints](https://go.dev/ref/spec#Type_constraints)).

---

## 5. Type Inference — You Usually Don't Write the Type Arguments

Calling a generic function rarely needs explicit `[T]`: "In many cases the compiler can infer the type argument for `T` from the ordinary arguments," and when it does, "calling generic functions looks no different than calling ordinary functions" ([Intro Generics](https://go.dev/blog/intro-generics)).

```go
xs := []int{3, 1, 2}
slices.Sort(xs)            // inferred [int]; not slices.Sort[int](xs)
m := Max(3, 7)            // inferred [int]
f := Max(2.5, 1.5)       // inferred [float64]
```

Write the explicit type argument only when inference can't succeed (e.g. `make`-style functions with no argument to infer from). Spelling out an inferable type argument is noise.

---

## 6. Prefer the Standard Library: `slices`, `maps`, `cmp`

Before writing a generic helper, check whether `slices`, `maps`, or `cmp` already has it. These are the type parameters the Go team already wrote and tested:

```go
// WRONG — hand-rolled generic Contains, reinventing the stdlib
func Contains[T comparable](s []T, v T) bool {
	for _, x := range s {
		if x == v {
			return true
		}
	}
	return false
}

// RIGHT — the standard library function (Go 1.21+)
found := slices.Contains(s, v)             // func Contains[S ~[]E, E comparable](s S, v E) bool
slices.Sort(xs)                            // func Sort[S ~[]E, E cmp.Ordered](x S)
top := slices.Max(xs)                      // func Max[S ~[]E, E cmp.Ordered](x S) E
keys := slices.Sorted(maps.Keys(m))        // iterate keys, then sort
```

Note the stdlib signatures use the `[S ~[]E, E any]` shape: `S ~[]E` lets a *named* slice type (`type IDs []int`) pass while `E` names the element. The *data operations* on slices and maps — append-aliasing, nil-vs-empty, preallocation, `clear` — are owned by **[`go-slices-and-maps`](../go-slices-and-maps/SKILL.md)**; this skill only says "reach for those packages instead of your own type parameters."

---

## 7. Generic Data Structures — the Legitimate Case

The clearest place a type parameter earns its keep is a reusable container the language doesn't provide: "A general purpose data structure is something like a slice or map, but one that is not built into the language, such as a linked list, or a binary tree." Replacing an interface element with a type parameter "can permit data to be stored more efficiently ... it can also permit the code to avoid type assertions, and to be fully type checked at build time" ([When To Use Generics](https://go.dev/blog/when-generics)).

```go
// A type-safe stack: the same code for every element type, fully checked at build time.
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
}
```

The same goes for functions over container element types — a `Map`/`Filter` over a slice, or `MapKeys` over a map — where "the function code doesn't make any particular assumptions about the element types" ([When To Use Generics](https://go.dev/blog/when-generics)). Note `var zero T` is the idiom for the zero value of a type parameter; see **[`go-zero-values-and-construction`](../go-zero-values-and-construction/SKILL.md)**.

---

## 8. Generic Aliases (1.24) and Self-Referential Constraints (1.26)

Two language features gate on the module's `go` directive — confirm the version before using them (**[`go-version-feature-map`](../go-version-feature-map/SKILL.md)** owns the table):

- **Generic type aliases (Go 1.24):** "Go 1.24 now fully supports generic type aliases: a type alias may be parameterized like a defined type" ([Go 1.24 release notes](https://go.dev/doc/go1.24)). `type Set[T comparable] = map[T]struct{}` is now legal.
- **Self-referential constraints (Go 1.26):** "The restriction that a generic type may not refer to itself in its type parameter list has been lifted" ([Go 1.26 release notes](https://go.dev/doc/go1.26)). This enables the recursive-constraint idiom:

```go
type Adder[A Adder[A]] interface {
	Add(A) A
}

func sum[A Adder[A]](x, y A) A { return x.Add(y) }
```

Both are advanced; reach for them only when a concrete type or a plain constraint can't express the relationship.

---

## 9. Who Suffers When Generics Are Done Badly

The cost of a needless type parameter is paid downstream, never by the author at write time:

- The **reviewer** forced to decode a `func Process[T constraint, U any, V ~[]U](...)` signature — three type parameters and a custom constraint — to make a one-line change, when a concrete function or a small interface said the same thing plainly. "Clear is better than clever" ([Go Proverbs](https://go-proverbs.github.io/)) is the empathy rule: the clever generic offloads its complexity onto whoever reads it next.
- The **teammate** who copies your hand-rolled generic `Contains`/`Map` into a third package because it was easier than finding it — duplicating what `slices` already ships, tested.
- The **next caller** of a `func(v T) string` whose body only calls `v.String()`: they get a confusing instantiation error where a plain `fmt.Stringer` parameter would have just worked.

Generics also carry a real implementation cost (monomorphization vs dictionaries, slower builds, harder-to-read errors), so the bar is "clear AND correct," not "as general as possible." When in doubt, write the concrete code.

---

## 10. Routing to the Specific Skills

- **[`go-idiomatic-discipline`](../go-idiomatic-discipline/SKILL.md)** — the policy root. Premature generics are its *axis 2* (over-abstraction); this skill holds the depth behind "don't reach for generics first."
- **[`go-interfaces`](../go-interfaces/SKILL.md)** — the other half of the boundary. If you only call a method, an interface is simpler than a type parameter; `any` says nothing. Coordinate the generic-vs-interface decision with §5 there.
- **[`go-slices-and-maps`](../go-slices-and-maps/SKILL.md)** — the `slices`/`maps` data operations the helpers in §6 belong to (aliasing, nil-vs-empty, preallocation, `clear`).
- **[`go-zero-values-and-construction`](../go-zero-values-and-construction/SKILL.md)** — `var zero T` for a type parameter's zero value, and useful zero values generally.
- **[`go-version-feature-map`](../go-version-feature-map/SKILL.md)** — generic aliases (1.24) and self-referential constraints (1.26) are gated on the `go` directive.

---

## 11. Reference Files

High-frequency generics anti-patterns in LLM-generated Go, each with wrong/right code and citations:

[references/common-mistakes.md](references/common-mistakes.md)

Source provenance for every claim in this skill:

[references/sources.yaml](references/sources.yaml)

