Type Parameters & Constraint Interfaces
Type parameters are declared in bracket lists, and constraint interfaces define which types may replace them using methods, underlying types (~T), and unions (|).
Search across all documentation pages
Type parameters are declared in bracket lists, and constraint interfaces define which types may replace them using methods, underlying types (~T), and unions (|).
Type parameters turn concrete types into compile-time variables for functions and named types.
Constraint interfaces are the guardrails: they list allowed operations and type sets.
The ~ operator matches named types by underlying type; | unions multiple types or underlying types into one constraint.
Named constraint types keep APIs readable when the same bound appears in many signatures.
Quick-reference recipe card - copy-paste ready.
// Named constraint - reuse across APIs
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
func Clamp[T Ordered](v, lo, hi T) T {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}When to reach for this:
~byte).int | string is invalid for shared ops - design carefully).package main
import (
"fmt"
"strconv"
)
// Method constraint
type Stringable interface {
String() string
}
// Underlying type + union
type Digit interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type UserID int
func (id UserID) String() string {
return strconv.Itoa(int(id))
}
func FormatAll[T Stringable](items []T) string {
out := ""
for i, v := range items {
if i > 0 {
out += ", "
}
out += v.String()
}
return out
}
func Twice[T Digit](v T) T {
return v * 2
}
func main() {
ids := []UserID{1, 2, 3}
fmt.Println(FormatAll(ids))
fmt.Println(Twice(UserID(5)))
}What this demonstrates:
v.String() inside the generic body.~int lets UserID satisfy Digit without listing every named int type.Stringable vs Digit).func F[T any]()) or types (type Stack[T any] struct{}).any.| Form | Example | Matches |
|---|---|---|
| Method list | interface { Read([]byte) (int, error) } | Types with Read method |
| Underlying type | ~int | int, MyInt, any named type with underlying int |
| Type literal | int | Only the literal type int, not named MyInt |
| Union | ~int | ~float64 | Named or literal ints and floats per member rules |
| Mixed | interface { ~int; String() string } | Underlying int and String() method |
| Syntax | Meaning |
|---|---|
[T any] | Single parameter, unconstrained |
[T, U comparable] | Two parameters, independent constraints |
[T Ordered, U any] | Mix custom and predeclared constraints |
func (s *Set[T]) Add(v T) | Method uses struct's T, no new parameter list |
// Extract constraints to package-level names for public APIs
type JSONMarshaler interface {
MarshalJSON() ([]byte, error)
}
func Encode[T JSONMarshaler](v T) ([]byte, error) {
return v.MarshalJSON()
}
// Embedding predeclared constraints
type Key interface {
comparable
fmt.Stringer
}<, the body cannot use < on T.cmp.Ordered and cmp.Or from stdlib instead of copying large union lists.int instead of ~int - Named types like type Celsius float64 will not match float64 alone. Fix: use ~float64 when named types should qualify.int | string cannot use + or shared comparisons beyond == if not comparable together. Fix: narrow the union or split functions.comparable incorrectly - interface { comparable; []byte } is invalid because slices are not comparable. Fix: list only comparable type terms.[T any, T comparable] is invalid. Fix: one name per parameter in the list.| Alternative | Use When | Don't Use When |
|---|---|---|
cmp.Ordered / stdlib constraints | Ordering and equality helpers | Domain-specific non-ordered types |
Code generation (go generate) | Many types, zero generic syntax in API | Small helper count, team knows generics |
interface{} / any + type switch | Truly dynamic shapes (JSON, plugins) | Hot paths needing static typing |
| Separate non-generic functions | Only two concrete types | Many types with identical logic |
~T means "any type whose underlying type is T."
It includes named types defined as type MyInt int.
Yes with |, but every operation in the generic body must be valid for all union members.
Wide unions often fail compile checks.
Export when callers must name them for their own generics.
Keep internal constraints unexported and stable.
It is a stdlib constraint interface listing ordered built-in and underlying types.
Prefer importing cmp over copying the union.
Yes - interface embedding works like ordinary interfaces.
Method sets combine per Go interface rules.
any allows all types but not == unless you narrow.
comparable allows equality operations on permitted types.
Interface types cannot declare type parameters in Go.
Use generic functions or generic structs instead.
Read the compiler message for which operation failed on which type argument.
Reduce the union or add required methods to the constraint.
No - instantiations are compile-time.
Reflection sees concrete instantiated types, not parameter names.
When the same bound appears in three or more signatures or documents a domain concept (for example NodeID).
No - implicit satisfaction rules are unchanged.
Constraints are interfaces used at compile time.
No - only generic types carry parameters into methods.
Free functions and generic types declare parameters.
cmp.Ordered usageStack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026