skip to content

Generics and Type Parameters

Type parameters, added in Go 1.18, and the constraint system that decides what generic code may do with them. Interviewers ask about generics both to test recent-Go knowledge and to see whether you can say when an interface is still the better answer.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

25

In Go generics, what is a type constraint, and what can a function do with a value of a type parameter constrained by any?

level: juniorimportance: must knowfreq 72%

answer

  1. an interface used in a new position
  2. it names a set of types, not just methods
  3. the body may use only what every member allows
  4. any admits everything, so it permits almost nothing

basics

~20 s

A constraint is an interface that bounds a type parameter, defining which types may be used as the type argument. The any constraint admits every type, so the body may only assign, pass and store such values, not compare or add them.

solid answer

~50 s

Every type parameter carries a constraint, and a constraint is always an interface. Since generics, an interface describes a **type set**: a method-only interface like `fmt.Stringer` has the type set of all types with that method, and a constraint may also list types directly, as in `interface { int | float64 }`. The rule that follows is the one people miss: an operation is legal on a value of type parameter type `T` only if it is legal for *every* type in the set. So under `[T any]` you can copy the value, pass it on, store it in a slice or return it, but `x + y`, `x < y` and even `x == y` do not compile, because `any`'s type set includes structs, slices and maps. Constraints are checked entirely at compile time when the function is instantiated; nothing is verified at run time.

code

go · 17 lines
go
// A constraint interface whose type set is int and float64.
type Number interface {
	int | float64
}

func TotalScore[T Number](scores []T) T {
	var total T
	for _, s := range scores {
		total += s // legal: + works for every type in the set
	}
	return total
}

func CountEntries[T any](entries []T) int {
	// total += e would not compile: any permits no operators
	return len(entries)
}

go deeper

for a junior

Be ready to say that a constraint is an interface naming the set of permitted types, and that under any you can only move a value around, not compare or add it.

for a middle

Explain that an interface's elements intersect to form a type set, and that an operator is legal on T only when every type in that set supports it with the same meaning.

for a senior

Show judgment about tightness: a constraint that merely restates any adds a type parameter without buying anything, while an over-narrow union locks out callers' defined types.

for a principal

Own the fact that a constraint written into an exported signature is part of the package's compatibility surface: widening it later is usually safe, narrowing it breaks every caller's instantiation.

## What a constraint is A generic declaration in Go lists **type parameters** in square brackets, each with a constraint: ```go func TotalScore[T Number](xs []T) T type Board[K comparable, V any] struct{ ... } ``` `Number`, `comparable` and `any` are the constraints. A constraint is always an interface — generics did not add a new kind of declaration for them. ## Interfaces got a second job: the type set Before generics an interface meant a set of methods, and a value satisfied it when its method set contained them. Generics reinterpreted every interface as defining a **type set** — the set of types that are elements of it. - A method-only interface such as `fmt.Stringer` has as its type set every type with a `String() string` method. Used as a constraint, it admits exactly those types. - A constraint may instead (or additionally) list types directly. `interface { int | float64 }` is a **union** whose type set is those two types. - `~int` is an **approximation term**: every type whose underlying type is `int`, so a caller's `type Score int` is included. - `comparable` is a predeclared constraint whose type set is the types on which `==` is defined and cannot panic. - Elements can be combined; the type set of an interface with several elements is their **intersection**. `any` is the predeclared alias for `interface{}`. As a constraint it places no restriction at all: every type argument is accepted. ## What the body may do with a T This is the practical half of the answer. Inside the generic body, an operation on a value of type parameter type `T` is permitted only when it is permitted for **every** type in `T`'s type set, with the same meaning. That gives a sharp trade: | Constraint | Legal on a T value | |---|---| | `any` | assign, pass, return, store, take the address, use in a slice or map value position | | `comparable` | the above, plus `==` and `!=`, plus use as a map key | | `cmp.Ordered` | the above, plus `<`, `<=`, `>`, `>=` and `+` (all its members support them) | | `interface{ int \| float64 }` | arithmetic operators supported by both members | | `fmt.Stringer` | the method, plus the `any`-level operations | So `[T any]` is the weakest possible bound: it says "I will not look inside this value". A function like `Count[T any](xs []T) int` or `First[T any](xs []T) T` is honest under `any`; a function that needs to add scores together is not, and must ask for a narrower type set. A common beginner surprise is that `x == y` does not compile under `any`. Slices, maps and functions have no `==`, and they are in `any`'s type set, so the compiler refuses the whole operation rather than deciding per instantiation. ## Constraints are a compile-time device When you instantiate a generic function or type, the compiler checks that each type argument satisfies its constraint and reports something of the form "X does not satisfy C" if it does not. There is no run-time check, no reflection, and no per-constraint boxing decision you control. If it compiles, the constraint is honoured for that instantiation. ## A constraint interface is not always a usable type An interface that contains anything other than methods — a union, a `~T` term, or `comparable`, directly or embedded — may be used **only** as a constraint. Declaring `var n Number` where `Number` is `interface{ int | float64 }` is a compile error. A method-only interface has no such restriction and can serve in both roles, which is why `fmt.Stringer` works as a constraint and as a variable type. ## Picking a constraint Start from what the body actually does and pick the weakest constraint that permits it. `any` when you only move values around; `comparable` when you key a map or de-duplicate; `cmp.Ordered` when you compare magnitudes; a union such as `~int64 | ~float64` when you do arithmetic on a caller's numeric types; a method-only interface when you need behaviour rather than representation. Over-tight constraints lock out callers' defined types for no gain; over-loose ones will not compile once the body grows.

  • Can a constraint interface also be used as the type of an ordinary variable?
    Only if it lists nothing but methods. An interface containing a union, a `~T` term or `comparable` — directly or embedded — may be used only as a constraint, and declaring a variable of that type is a compile error. `fmt.Stringer` has no such restriction and works in both positions.
  • What exactly is any, and how does it differ from interface{}?
    `any` is a predeclared alias for `interface{}`, introduced with generics in Go 1.18. It is the same type, not a new kind of thing; the alias exists because `[T any]` reads better than `[T interface{}]`. As a constraint it accepts every type argument.
  • Why does the compiler reject total += x when T is constrained by any?
    An operator is allowed on a type parameter only when every type in its type set supports it. `any`'s type set includes structs, slices, maps and functions, none of which support `+`, so the compiler rejects the expression outright rather than deferring the decision to each instantiation.

A constraint is a guest list, not a job description: it says which types may come in, and the body may only ask for what every guest can do.

saying these in an interview costs you the question

  • Says a constraint is just an interface listing methods
  • Thinks any lets the body use == or +
  • Believes constraints are checked at run time by reflection
  • Expects a union constraint to work as a variable type
open as a page

When does Go infer a generic function's type arguments, and when must you write F[int] yourself?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Go infers a generic function's type arguments from the ordinary arguments you pass at the call site. A type parameter that appears only in the result is not determined by any argument, so you must write it explicitly, as in Zeroint.

open as a page

Why does a Go function that returns `any` push work onto every caller, and how does a type parameter remove it?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Returning any gives the caller a value with no static type, so every call site must type-assert and can panic at run time. A type parameter returns the caller's own type instead, so the compiler checks the use.

open as a page

In Go, what does the `[T any]` in `func Reverse[T any](s []T)` declare, and where can T be used?

level: juniorimportance: must knowfreq 70%

basics

~20 s

The brackets declare a type parameter list. T is a placeholder for a type, constrained by any, so one Reverse serves every element type. T is in scope for the rest of the signature and the whole body.

open as a page

When do you constrain a Go type parameter with comparable rather than cmp.Ordered, and what does each permit?

level: middleimportance: must knowfreq 62%

basics

~20 s

Use comparable when the code needs equality: it is the predeclared constraint for types supporting == and !=, which is what a map key or set element requires. Use cmp.Ordered when the code needs ordering with <, >: integers, floats and strings.

open as a page

What can a `[T fmt.Stringer]` type parameter express that a plain `fmt.Stringer` parameter cannot?

level: middleimportance: must knowfreq 62%

basics

~10 s

A type parameter keeps the caller's concrete type, so it can appear in the result, in a second parameter, or as a slice element type. An interface parameter erases it, leaving only the methods.

open as a page

How would you write a generic helper `Map[T, U any](s []T, f func(T) U) []U`, and how should it allocate its result?

level: middleimportance: must knowfreq 58%

basics

~20 s

Two type parameters carry the input and output element types. Allocate with make([]U, 0, len(s)) and append one result per element: the length is known exactly, so preallocating capacity avoids regrowth. Using make([]U, len(s)) with append instead doubles the result.

open as a page

In Go, what does it mean for a method to declare its own type parameter?

level: juniorimportance: should knowfreq 22%

basics

~20 s

Since Go 1.27 a method may list a type parameter after its own name, and each call site picks a type for it. That is different from a method on a generic type, which declares nothing new and simply reuses the parameter its receiver type was instantiated with.

open as a page

In a generic function `Find[T any](rows []T, match func(T) bool) (T, bool)`, how do you produce the T you return when nothing matches?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Declare a variable of the type parameter and return it: var zero T, then return zero, false. That is the zero value of whatever type T was instantiated with. No literal, not nil, 0 or T{}, is valid for every T.

open as a page

What does the tilde in a Go constraint element like ~int mean, and why does a defined type fail int | string?

level: middleimportance: should knowfreq 58%

basics

~20 s

The term ~int means any type whose underlying type is int, so it matches int and defined types such as type PlayerID int. A bare int term matches only int itself, which is why a defined type is rejected by int | string.

open as a page

Why must a Go generic type such as Stack[T] always be written with explicit type arguments?

level: middleimportance: should knowfreq 48%

basics

~20 s

Instantiating a generic type is not a function call, so there are no argument values whose types the compiler could match against. Every use of the type must name its type arguments: var s Stack[int], not var s Stack.

open as a page

Why can a Go interface never declare a method that has its own type parameter?

level: middleimportance: should knowfreq 30%

basics

~20 s

An interface value dispatches through a table holding one function pointer per method, filled in when a concrete type is assigned. A method whose type argument is picked at the call site has an unbounded family of instantiations, so there is no single entry to store. Interfaces therefore forbid it.

open as a page

How do you declare a generic type such as `type Cache[K comparable, V any] struct`, and what must its methods repeat?

level: middleimportance: should knowfreq 55%

basics

~20 s

Put the type parameter list right after the type name; the parameters are usable in every field. Each method must redeclare them on the receiver, as in func (c *Cache[K, V]) Get(k K), and every instantiation is a distinct type.

open as a page

Your generic test helper fails with "cannot infer Out". How do you diagnose it and reshape the signature?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The error names the type parameter the compiler could not solve. Find it in the signature: if it appears only in the result, no argument determines it. The fix is a parameter that mentions it, not type arguments at every call.

open as a page

What breaks for consumers when an exported Go function or type gains a type parameter in a new release?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Adding a type parameter is a breaking API change. Code using the function as a value stops compiling until it is instantiated, and every mention of a newly generic type must name its type arguments.

open as a page

A build cache server exports `type Cache[K comparable, V any]`; a new teammate's `var c Cache` will not compile. Why, and how would you shape the API?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A generic type name is not a type until it is instantiated, so every use needs type arguments. Export a constructor and an alias for the instantiation you support, and pin it with a compile-time check.

open as a page

A CPU profile blames a generic helper's per-element `func(T) U` callback in a batch job. Why is it slower than the loop it replaced, and how do you fix it without changing behaviour?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The generics are rarely the cost. The callback is an indirect call the compiler usually cannot inline, once per element, where the old loop inlined its body. Benchmark it, read go build -gcflags=-m, then specialise the hot path.

open as a page

You own a Go feature-flag SDK a dozen teams import: how do you decide between a generic API and an interface-based one?

level: principalimportance: should knowfreq 34%

basics

~20 s

Decide by reversal cost, not elegance. Keep the core surface non-generic and offer typed convenience as additive generic helpers, so a shape you got wrong changes without a v2 module path and a dozen migrations.

open as a page

Can a Go constraint require both a method and a specific underlying type, and what satisfies such a constraint?

level: middleimportance: nice to knowfreq 32%

basics

~20 s

Yes. A constraint interface may hold a union of type terms and method requirements as separate elements, and its type set is their intersection. Only a defined type with one of those underlying types that also declares the method qualifies.

open as a page

Given func Min[T cmp.Ordered](a, b T) T, what does Go infer for Min(1, 2), and why can that surprise you?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

T is inferred as int, so the call returns an int. Untyped constants contribute their default type to inference, and int is the default for 1 and 2 — the constraint permitting float64 does not make the compiler choose it.

open as a page

When is `func F[S ~[]E, E any](s S)` worth its second type parameter over `func F[E any](s []E)`?

level: middleimportance: nice to knowfreq 33%

basics

~10 s

The extra parameter earns its place only when the function must produce the caller's named slice type. If nothing of type S comes back out, take a plain []E and keep the signature readable.

open as a page

Why does `slices.Clone` declare `[S ~[]E, E any]` instead of one `[E any]` with a `[]E` parameter?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

Two type parameters name both the caller's slice type and its element type. S is the actual argument type, including a named type such as Names, so the result keeps that type instead of a plain []string.

open as a page

In Go, before writing your own generic Contains or SortFunc helper, how do you check the standard library already ships it?

level: middleimportance: nice to knowfreq 35%

basics

~20 s

Run go doc slices and go doc maps, or go doc slices.Contains for one symbol. Since Go 1.21 those packages ship generic search, sort, clone and compare helpers, so most hand-rolled versions are duplicates that drift.

open as a page

Why does a Go container declared with [K comparable] accept K = any, and how can that panic at run time?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

comparable's type set is the strictly comparable types, but Go's satisfaction rule makes an exception for merely comparable type arguments, so an interface type such as any is accepted. Comparing or hashing an interface holding a slice then panics at run time.

open as a page

Your Adapter.Decode method gained its own type parameter and Adapter no longer satisfies your exported Decoder interface. How do you reshape the API?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Move the type parameter off the method. Put it on a package-level generic function that takes the adapter as an argument, or on the interface type itself, and leave the method with a concrete signature so it keeps satisfying the interface. A parametric method can never implement an interface method.

open as a page