---
title: Type constraints
summary: Type constraints in Go define the set of allowed types for generic parameters using interfaces or type unions.
lang: en
slug: type-constraints
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.gofaq.org/en/type-constraints/
---

# Type constraints



## Type constraints

You write a function to find the maximum of two values. It works for integers. Then you need it for floats. You copy the code, change `int` to `float64`, and run. A week later, you need it for custom structs that implement a comparison method. Copy-pasting code is a trap. Go generics let you write the function once, but the compiler needs to know what `T` is allowed to be. Type constraints are the rules that tell the compiler which types fit into your generic template.

Without constraints, a type parameter is `any`. That means the compiler allows you to pass a channel, a function, or a struct. If your function tries to add two values, the compiler can't guarantee the operation exists. Constraints tighten the scope. They define a type set that the compiler checks against every call site. If the argument type isn't in the set, the build fails.

Think of a constraint like a bouncer at a club. The club is your generic function. The bouncer checks IDs. If the ID matches the list, you get in. If not, the bouncer kicks you out. In Go, the bouncer is the compiler, and the ID list is the type constraint. The constraint ensures that every type used with your generic code supports the operations you need.

Constraints are compile-time guards. They vanish at runtime. The generated code has no overhead.

## Defining constraints with interfaces

Constraints look like interfaces, but they behave differently. You define a constraint using the `interface` keyword, but you never assign a value to it. The compiler uses the constraint to build a type set. When you call a generic function, the compiler checks if the argument's type is in that set. If you try to use a constraint as a regular interface, the compiler stops you. Constraints are metadata for the type checker, not runtime types.

Here's the simplest constraint: an interface that lists allowed types.

```go
// Number defines a constraint for numeric types.
// The ~ prefix allows underlying types, not just the named type.
type Number interface {
    ~int | ~float64
}

// Sum adds a slice of numbers and returns the total.
// T is constrained to Number, so the compiler guarantees T supports addition.
func Sum[T Number](nums []T) T {
    var total T // Zero value of T works because all numbers have a zero value.
    for _, n := range nums {
        total += n // Operator + is valid because T is constrained to numeric types.
    }
    return total
}
```

The `Number` constraint uses a union of types. The `|` operator combines `~int` and `~float64`. The `~` prefix is crucial. It means "this type or any type whose underlying type is this." Without `~`, the constraint would only accept the exact types `int` and `float64`. With `~`, it also accepts `type MyInt int` or `type Celsius float64`.

The `any` keyword is just an alias for `interface{}`. Use `any` in constraints for readability. The community prefers `any` over the empty interface syntax in generic contexts.

Constraints are contracts. Break the contract and the compiler breaks the build.

## The tilde and underlying types

The tilde operator expands a constraint to include custom types. Go allows you to define new types based on existing ones. `type MyInt int` creates a distinct type. `MyInt` is not `int`. They have the same underlying representation, but the compiler treats them as different.

If you write a constraint with `int`, only `int` satisfies it. `MyInt` fails. If you write `~int`, both `int` and `MyInt` satisfy it. This distinction matters when you want your generic code to work with domain-specific types.

```go
// WithoutTilde shows why the tilde matters.
// This constraint only accepts the exact type int.
type ExactInt interface {
    int
}

// WithTilde accepts int and any type with underlying type int.
type IntLike interface {
    ~int
}

type MyInt int

func processExact[T ExactInt](v T) {
    // Works for int, fails for MyInt.
}

func processLike[T IntLike](v T) {
    // Works for both int and MyInt.
}
```

If you call `processExact(MyInt(5))`, the compiler rejects this with an error stating that `MyInt` does not implement the `ExactInt` constraint. The compiler sees `MyInt` as a different type. If you call `processLike(MyInt(5))`, it compiles. The underlying type of `MyInt` is `int`, which matches `~int`.

Use the tilde when you want flexibility. Skip it when you need strictness. The tilde is the difference between rigid and flexible. Use it when you want custom types to play along.

## Methods in constraints

Constraints can include methods. If you add a method to a constraint, any type satisfying the constraint must implement that method. This lets you combine type requirements with behavior requirements.

```go
// StringerInt constrains T to be an int-like type that also has a String method.
type StringerInt interface {
    ~int
    String() string
}

// LogInt prints the value using its String method.
func LogInt[T StringerInt](v T) {
    // v.String() is safe because the constraint requires the method.
    fmt.Println(v.String())
}
```

The `StringerInt` constraint requires two things. The type must have an underlying type of `int`. The type must have a `String() string` method. A plain `int` fails because it doesn't have a `String` method. A `type MyInt int` with a `String` method succeeds.

You can mix types and methods in any combination. The compiler builds a type set that includes all types matching the union of types and all types implementing the method set. This is powerful for library code that needs specific operations on custom types.

Constraints are compile-time guards. They vanish at runtime. The generated code has no overhead.

## The comparable constraint

The `comparable` constraint is built into the language. It represents all types that support `==` and `!=`. This includes basic types, structs with comparable fields, pointers, and interfaces. You cannot define `comparable` yourself. It's a primitive constraint. Use it whenever you need map keys or equality checks.

```go
// Unique returns a slice with duplicate values removed.
// The comparable constraint ensures T can be used as a map key.
func Unique[T comparable](items []T) []T {
    seen := make(map[T]struct{}) // Empty struct uses zero memory for values.
    var result []T
    for _, item := range items {
        if _, ok := seen[item]; !ok {
            seen[item] = struct{}{} // Mark item as seen.
            result = append(result, item)
        }
    }
    return result
}
```

The `comparable` constraint is essential for algorithms that rely on equality. If you try to use a slice or map as a type parameter without `comparable`, and you use it as a map key, the compiler rejects this with an error stating that the type is not comparable. Slices and maps are not comparable in Go, so they don't satisfy `comparable`.

Public names start with a capital letter. Private start lowercase. Constraints follow the same rule. `Addable` is public, `addable` is private. Name constraints descriptively. `Number` is better than `N`. `Stringer` is better than `S`.

## Realistic example: Grouping by key

Real applications often combine multiple constraints. This function groups a slice by a key. The key type `K` must be comparable. The value type `V` can be anything.

```go
// GroupBy groups items by a key function.
// K must be comparable to use as a map key.
// V is any type.
func GroupBy[K comparable, V any](items []V, keyFn func(V) K) map[K][]V {
    result := make(map[K][]V)
    for _, item := range items {
        k := keyFn(item)
        result[k] = append(result[k], item)
    }
    return result
}
```

The `GroupBy` function takes two type parameters. `K` is constrained to `comparable` because it becomes a map key. `V` is constrained to `any` because the function doesn't care about the value type. The `keyFn` argument extracts the key from each value. This pattern is common in data processing.

Gofmt handles constraint formatting. It aligns the type list. Don't fight it. Most editors run `gofmt` on save. Trust the tool.

Type sets are the engine. Interfaces are the syntax. Understand the set to master the constraint.

## Pitfalls and errors

Constraints can be tricky. The most common mistake is forgetting the tilde. If you define a constraint with `int` and pass a custom type, the compiler rejects this with an error saying the type does not satisfy the constraint. Add `~` to fix it.

Another pitfall is using a constraint as a value. You cannot create a variable of type `Number` and assign an int to it. The compiler rejects this with an error that the constraint cannot be used as a value type. Constraints are only for type parameters.

Method sets in constraints can also cause confusion. If you add a method to a constraint, the type must implement it. If you have `type MyInt int` without a `String` method, it fails a constraint that requires `String()`. The compiler rejects this with an error stating the method is missing.

Constraints are compile-time guards. They vanish at runtime. The generated code has no overhead.

## When to use constraints

Use `any` when the function works with any type and doesn't rely on specific operations. Use `comparable` when you need to use the type as a map key or compare it with `==`. Use a union of types like `int | float64` when you need to perform operators like `+` or `-` that only work on specific types. Use the `~` prefix when you want to allow custom types that share an underlying type, like `type Celsius float64`. Use a method-based constraint when the function needs to call a specific method, like `String()` or `Read()`. Use a type set with both types and methods when you need a custom type that also implements a behavior.

Use the right constraint. `any` is lazy. `comparable` is precise. Unions are specific. Pick the tightest fit.

## Where to go next

- [Type sets in generics](/en/type-sets-in-generics/)
- [How to Solve LeetCode Problems in Go: Tips and Patterns](/en/how-to-solve-leetcode-problems-in-go-tips-and-patterns/)
- [What Are the Go Release Cycle and Version Numbering](/en/what-are-the-go-release-cycle-and-version-numbering/)