How to Use the constraints Package in Go

Import golang.org/x/exp/constraints to access generic type constraints like Ordered and Comparable for writing flexible Go functions.

The problem with untyped generics

You write a function to find the maximum of two numbers. It works for int. You copy it for float64. You copy it again for string because you suddenly need lexicographic comparison. Three identical functions, three different type signatures. Go generics let you collapse them into one, but the type parameter needs rules. Without rules, the compiler refuses to let you use < or + on a bare generic type T. That restriction exists for a reason: the compiler cannot verify at compile time whether an arbitrary type supports arithmetic or comparison. The constraints package fills that gap by giving you prebuilt interface definitions that tell the compiler exactly what operations a type must support.

What the constraints package actually is

The constraints package lives at golang.org/x/exp/constraints. It is not part of the standard library. The Go team places experimental and community-driven code in the golang.org/x/ repository tree. The exp subdirectory signals that the API is still being shaped by real-world usage. These interfaces define the minimum capabilities a generic type parameter must have. Ordered means the type supports comparison operators like <, >, <=, >=, ==, and !=. Integer and Float restrict types to numeric categories. Comparable is broader: it only requires == and !=. You import it like any other third-party module, but it carries the weight of official Go design philosophy.

Go prefers explicit contracts over implicit magic. The language does not bundle numeric constraints into the core std package because the team wants to observe how developers actually use them before committing to a permanent shape. Until that decision lands, golang.org/x/exp/constraints is the de facto standard. Treat it like a first-class citizen in your codebase. Run go get golang.org/x/exp/constraints once, and the module system handles the rest.

Generics are compile-time only. There is no runtime cost for using constraints.

A minimal working example

Here is the simplest way to attach a constraint to a generic function. The code below defines a Max function that accepts any type satisfying the Ordered constraint.

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

// Max returns the larger of two ordered values.
func Max[T constraints.Ordered](a, b T) T {
	// The constraint guarantees T supports the < operator.
	if a < b {
		return b
	}
	// Fallback returns the first value when they are equal or a is larger.
	return a
}

func main() {
	// The compiler infers T as int from the arguments.
	fmt.Println(Max(3, 7))
	// The compiler infers T as float64 from the arguments.
	fmt.Println(Max(3.14, 2.71))
	// The compiler infers T as string and compares lexicographically.
	fmt.Println(Max("apple", "banana"))
}

The T constraints.Ordered syntax tells the compiler to accept any type that implements the Ordered interface. The standard library types int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr, float32, float64, and string all satisfy it. You do not need to write boilerplate interface implementations. The compiler recognizes these built-in types automatically.

Constraints are compile-time contracts. The runtime never checks them.

How the compiler handles it

When you call Max(3, 7), the compiler substitutes T with int. It verifies that int satisfies constraints.Ordered. If it does, the compiler generates a dedicated Max function specifically for int. This process is called monomorphization. If you later call Max(3.14, 2.71), the compiler generates a second, separate function for float64. Each concrete type gets its own optimized copy. The constraint itself disappears after compilation. It exists only to guide type checking and code generation.

This approach keeps Go generics fast. There is no interface boxing, no virtual dispatch, and no reflection. The generated code runs exactly as if you had written separate functions by hand. The tradeoff is that the compiler must see all call sites at build time. You cannot pass a generic function as a value without specifying the type parameter, and you cannot create a slice of mixed generic types without a common concrete type.

Monomorphization means zero runtime overhead. Write the constraint once, get optimized code for every type.

Real-world usage: clamping values

Constraints shine when you need to enforce bounds on numeric or string data. Consider a configuration parser that reads user input and needs to restrict values to a safe range. A generic Clamp function eliminates repetition across different configuration fields.

package main

import (
	"fmt"
	"golang.org/x/exp/constraints"
)

// Clamp restricts a value to stay within a lower and upper bound.
func Clamp[T constraints.Ordered](value, min, max T) T {
	// If the value is below the minimum, return the minimum.
	if value < min {
		return min
	}
	// If the value exceeds the maximum, return the maximum.
	if value > max {
		return max
	}
	// The value is already within bounds.
	return value
}

func main() {
	// Clamps an integer configuration value between 1 and 100.
	fmt.Println(Clamp(0, 1, 100))
	// Clamps a float64 temperature reading between -273.15 and 100.0.
	fmt.Println(Clamp(-300.0, -273.15, 100.0))
	// Clamps a string alphabetically between "A" and "Z".
	fmt.Println(Clamp("M", "A", "Z"))
}

The function works identically for integers, floats, and strings because they all satisfy Ordered. You can drop this into a validation layer, a game engine, or a data pipeline without rewriting the logic. The constraint guarantees that the comparison operators are valid, so the compiler catches type mismatches before the program runs.

Keep constraints tight. Restrict the type parameter to exactly what the function needs.

Common mistakes and compiler feedback

Developers new to Go generics often try to use constraints on custom structs without realizing what the compiler expects. If you define a Point struct and pass it to Clamp, the build fails with invalid operation: value < min (operator < not defined on T). The compiler cannot invent comparison logic for your type. You must either implement the comparison yourself using a custom interface, or stick to built-in types that already satisfy Ordered.

Another frequent issue involves forgetting to fetch the module. If you import golang.org/x/exp/constraints without running go get, the compiler rejects the file with could not import golang.org/x/exp/constraints (no required module provides package). The x/exp path is not bundled with the Go distribution. You must add it to your go.mod explicitly.

Some developers also confuse constraints.Ordered with any. Using any removes all restrictions. If you write func Max[T any](a, b T) T and try to use <, the compiler stops with invalid operation: a < b (operator < not defined on T). The any keyword is an alias for interface{}. It tells the compiler to accept literally anything, including channels, maps, and functions. None of those support comparison operators. Constraints exist precisely to prevent that mismatch.

Constraints are type-safe guards. Let the compiler enforce them instead of writing runtime checks.

When to reach for which constraint

Use constraints.Ordered when your function needs to sort, compare, or bound values using <, >, <=, >=, ==, or !=. Use constraints.Integer when you only need integer arithmetic and want to exclude floats and strings. Use constraints.Float when your logic requires decimal precision and fractional division. Use constraints.Comparable when you only need equality checks, such as building a generic cache key or deduplicating a slice. Use any when the function treats the value as an opaque blob, like a generic logger or a serialization wrapper. Use a custom interface when you need domain-specific behavior, like a Shape that must implement Area() float64.

Pick the narrowest constraint that covers your operations. Wider constraints invite invalid types and force you to add runtime guards.

Where to go next