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 (|).
Summary
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.
Recipe
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:
- You need one function body for several concrete types that share operators or methods.
- A named type should participate because its underlying type matches (
~byte). - Multiple unrelated types must be accepted via a union (
int | stringis invalid for shared ops - design carefully).
Working Example
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:
- Method constraints enable calls like
v.String()inside the generic body. ~intletsUserIDsatisfyDigitwithout listing every named int type.- Separate constraints compose different capabilities (
StringablevsDigit).
Deep Dive
How It Works
- Type parameters are declared on functions (
func F[T any]()) or types (type Stack[T any] struct{}). - Each parameter has a constraint interface; default unconstrained use is
any. - The compiler computes the constraint's type set; only types in that set may instantiate the generic.
- The function body may only use operations valid for all types in the set (the core type set rules).
Constraint Forms at a Glance
| 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 |
Type Parameter Lists
| 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 |
Go Notes
// 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
}- Constraint interfaces cannot embed arbitrary non-interface types except type terms in unions.
- A union may not mix incompatible operations - if one arm lacks
<, the body cannot use<onT. - Use
cmp.Orderedandcmp.Orfrom stdlib instead of copying large union lists.
Gotchas
- Using
intinstead of~int- Named types liketype Celsius float64will not matchfloat64alone. Fix: use~float64when named types should qualify. - Union too wide for one body -
int | stringcannot use+or shared comparisons beyond==if not comparable together. Fix: narrow the union or split functions. - Embedding
comparableincorrectly -interface { comparable; []byte }is invalid because slices are not comparable. Fix: list only comparable type terms. - Constraint interface with only methods, no type set - Empty interface method sets behave differently from unions; read spec for comparable core type sets. Fix: test instantiations in compile-time table tests.
- Exporting huge constraint unions - Public APIs become hard to read and stabilize. Fix: unexport constraint, export function with minimal bounds.
- Duplicate type parameters -
[T any, T comparable]is invalid. Fix: one name per parameter in the list.
Alternatives
| 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 |
FAQs
What does the tilde (~) mean in a constraint?
~T means "any type whose underlying type is T."
It includes named types defined as type MyInt int.
Can I union arbitrary types?
Yes with |, but every operation in the generic body must be valid for all union members.
Wide unions often fail compile checks.
Should constraints be exported?
Export when callers must name them for their own generics.
Keep internal constraints unexported and stable.
How is cmp.Ordered defined?
It is a stdlib constraint interface listing ordered built-in and underlying types.
Prefer importing cmp over copying the union.
Can constraints embed other interfaces?
Yes - interface embedding works like ordinary interfaces.
Method sets combine per Go interface rules.
What is the difference between any and comparable?
any allows all types but not == unless you narrow.
comparable allows equality operations on permitted types.
Can I put type parameters on interfaces?
Interface types cannot declare type parameters in Go.
Use generic functions or generic structs instead.
How do I debug constraint errors?
Read the compiler message for which operation failed on which type argument.
Reduce the union or add required methods to the constraint.
Are type parameters reified at runtime?
No - instantiations are compile-time.
Reflection sees concrete instantiated types, not parameter names.
When should I name a constraint type?
When the same bound appears in three or more signatures or documents a domain concept (for example NodeID).
Do generics change interface satisfaction?
No - implicit satisfaction rules are unchanged.
Constraints are interfaces used at compile time.
Can methods add new type parameters?
No - only generic types carry parameters into methods.
Free functions and generic types declare parameters.
Related
- Generics in Go: Constraints Without Templates - conceptual overview
- Generics Basics - first generic snippets
- Type Sets & the comparable Constraint - comparable deep dive
- Generic Algorithms: slices, maps & cmp Packages -
cmp.Orderedusage - When to Avoid Generics in Go APIs - public API guidance
Stack 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).