AlgoMaster Logo

Type Constraints

Medium Priority23 min readUpdated June 6, 2026

A type parameter on its own ([T any]) lets you store and pass values of type T, but you can't actually do much with them. You can't call t.Price(), you can't use t as a map key, and you can't compare two T values with ==. A type constraint is the contract that fixes this: it tells the compiler what the generic code is allowed to assume about T, and in return the compiler refuses to compile any call where the supplied type doesn't honour that contract. This lesson covers the two predeclared constraints (any and comparable), how to use any interface as a constraint when methods are all you need, and how to compose constraints with embedding.

Why Constraints Exist

When you write a generic function with [T any], you're telling the compiler "T could be anything." That sounds liberating, but it's also limiting. If T could be anything, the compiler has to assume the worst, which means almost no operations are safe on a value of type T.

That compiles, because fmt.Println accepts any. But the moment you try to do something more specific, things break:

The compiler refuses. From its point of view, T could be a bool, a []string, or a custom struct with no + operator. Without more information, it can't generate code that's safe for every possible T.

A constraint is how you give the compiler that information. Instead of [T any], you write [T SomethingMoreSpecific]. The "something more specific" is an interface that lists the operations a type must support to be used as T. Inside the function body, the compiler then knows those operations are safe to call.

For the rest of this chapter we'll work with a small e-commerce model. Products have a price, orders have a total, and we want generic helpers like "print all of these," "apply a discount to any of these," or "find the most expensive one." Each helper needs a different constraint, and walking through them shows what constraints actually buy you.

The compiler asks a single question for every generic call: "does the supplied type satisfy the constraint?" If yes, the call compiles. If no, you get an error at the call site, not deep inside the generic function. That early failure is exactly the point of constraints.

any and comparable

Go ships two predeclared constraints you can use without defining anything yourself. They cover the two ends of the spectrum: "I don't care what T is" and "I need to compare T values."

any

any is an alias for the empty interface, interface{}. As a constraint, it means "T can be any type." It's the weakest possible constraint and gives the function body the fewest powers.

First doesn't touch the value beyond returning it, so any is enough. The function works for slices of any element type without modification.

Use any whenever the body treats T as opaque. Containers (Stack[T any], Queue[T any]), simple pass-throughs (First, Last, Reverse), and helpers that only forward the value to fmt.Println or assign it to another variable all fit.

comparable

comparable is the second predeclared constraint. It allows == and != on values of type T. This sounds small but it unlocks two important capabilities: deduplication and using T as a map key.

item == target is only legal because T is comparable. If we'd written [T any], that line wouldn't compile.

A type is comparable if == is defined on it. Predefined types like int, string, float64, bool, and pointers are all comparable. Arrays of comparable element types are comparable. Structs whose fields are all comparable are comparable. Slices, maps, and functions are not comparable, and trying to use them as a comparable type parameter is a compile error.

Here's a generic deduplicator that also doubles as a demo of why comparable is exactly the right constraint:

The map[T]bool line is what forces T comparable. A Go map key type must be comparable. If you tried to constrain this with [T any], the compiler would reject map[T]bool with invalid map key type T.

The two predeclared constraints together cover a surprising amount of ground. Here's a quick map:

ConstraintWhat T can beWhat you can do with T
anyLiterally any typeStore, pass, assign, print via any-accepting APIs
comparableAny type whose values can be compared with ==Everything any allows, plus ==, !=, and use as map key

Interfaces as Constraints

A type constraint is just an interface. Anywhere you can write an interface type, you can also use it as a constraint. That includes interfaces from the standard library, interfaces you define for use as method sets, and interfaces you define purely for use as constraints. Go doesn't draw a hard line between "interface for runtime use" and "interface for constraint use," it's the same construct.

This is the simplest path to writing useful generic code: pick an existing methods-only interface, and let the generic function rely on those methods.

A familiar example is fmt.Stringer, defined as:

Any type that has a String() string method satisfies Stringer. Let's use it as a constraint:

PrintAll works for any element type that has a String() string method. Inside the function, the compiler knows item.String() is legal because the constraint guarantees it.

A reasonable question is "why not just pass []fmt.Stringer?" You can, but it's not quite the same thing. A plain []fmt.Stringer requires the caller to build a slice of interfaces, which means boxing each element and giving up the static type. With a generic function, the caller passes a []Product directly, and the function stays type-aware about what's inside. That distinction matters more once functions return values of type T, which interfaces would force you to type-assert.

The pattern generalises. If you've got an interface that captures a behaviour your generic code needs, just use it as the constraint. Here's a Priceable interface that models anything with a price:

Two different concrete types, one generic function. The function returns a T, so for []Product it returns a Product (with its Name field directly accessible), and for []Subscription it returns a Subscription (with its Plan field). No type assertions, no interface boxing.

What the compiler is checking at the call site:

When the caller writes MostExpensive(products), the compiler infers T = Product, then checks whether Product satisfies Priceable. Since Product has Price() float64, the check passes. If we tried to call MostExpensive([]int{1, 2, 3}), the compiler would reject it because int has no Price() method.

Defining Custom Constraint Interfaces

When no existing interface fits, you define your own. A constraint interface is just an interface. There's nothing special you need to do to mark it "for constraints only."

Suppose you want a generic helper that applies a discount to anything in the catalog. Different things have different discount logic: a product has a base price, an order has subtotal-with-shipping, a subscription has a monthly recurring fee. Each one knows its own price, and each one knows how to apply a discount to itself. Capture that as an interface:

Two methods: Price() to read the current price and ApplyDiscount to mutate it. We need a pointer receiver for ApplyDiscount because it has to modify the value:

Now the generic function. It takes a slice of T, where T is Discountable, and applies the discount to each one:

Putting it together:

Two things are worth highlighting in this design.

First, the slice element type is *Product (pointer), not Product. That's because ApplyDiscount has a pointer receiver, which means only *Product satisfies the Discountable interface, not Product. If you tried ApplyDiscount(products, 10) where products is []Product, the compiler would reject it because the value type Product doesn't have the method. This is the same value-vs-pointer method-set rule that applies to interfaces in general, and it shows up just as strictly when those interfaces are used as constraints.

Second, the constraint requires the type to do two things. If a type only has Price() but not ApplyDiscount, the constraint isn't satisfied. The compiler checks the full method set, not just one method:

The compiler error is precise. It names the missing method, so you know exactly what to add.

A common question is whether constraint interfaces should live next to the generic function or next to the concrete types. The answer is usually "next to the generic function," because the interface exists to serve that function. If the same interface also makes sense at runtime (var things []Discountable), or if it's used by more than one generic function, promote it to a top-level package declaration. We'll come back to that placement decision later.

Embedding Interfaces in Constraints

A constraint can embed other interfaces. Embedding combines their method sets: a type satisfies the combined constraint only if it satisfies every embedded interface.

This is useful when you want to build small, focused interfaces and combine them at the constraint site, rather than writing one giant interface upfront. Suppose you've got these two:

A generic catalog printer might want both: it prints a name and a price. Embed them:

A type satisfies Listable only if it has both Price() float64 and Name() string. Now a single generic function can rely on both methods:

Product and GiftCard both have both methods, so both satisfy Listable. PrintCatalog works for either slice type.

What's nice about embedding is that the two small interfaces are still useful on their own. You can write [T Priceable] for a function that only needs Price(), or [T Named] for one that only needs Name(), or combine them inline at the call site when you need both. The interfaces compose.

Here's a picture of what the compiler sees:

Listable is the combination of Priceable and Named. Product has both methods, so it satisfies Listable. Embedding is set union: the combined method set is the union of the embedded ones.

You can also embed an interface as part of a custom constraint without giving it a separate name. This is handy when you just want to "add comparable on top of an existing methods-only interface":

Identified embeds the predeclared comparable and adds an ID() int method requirement. So T must satisfy both: it must be comparable (otherwise we couldn't use it as a map key) and it must have an ID() int method. Order is a struct whose fields are all comparable, and it has the method, so it satisfies the constraint.

That trick (embedding comparable next to method requirements) is the cleanest way to express "this type needs to be a map key AND have these methods." Without embedding, you'd have to express it as two separate constraints, which Go's syntax doesn't really allow on one type parameter.

Methods on the Type Parameter

Once a constraint promises that T has certain methods, you can call those methods on values of type T inside the generic function body. The compiler treats it the same way it would treat a method call on a concrete type: it looks up the method through the constraint and generates the call.

Here's a slightly bigger example. We've got an Order type with line items, and an interface that says "I have a Total() float64 method." A generic Summary function uses that method:

Inside Summary, the line t := item.Total() works because the constraint Totalable promises every T has that method. The compiler doesn't care that the actual Total() implementations are different for Order and Refund. At instantiation time, it picks the right one for each concrete T.

Two important points about calling methods through a type parameter:

First, the method set the compiler considers is exactly the one declared in the constraint, no more. Even if every concrete T you pass has additional methods, the generic body can't call them. If you want Summary to also call item.Refund(), you'd have to add Refund() to Totalable. The constraint is the API contract for the function body.

Second, method dispatch through a type parameter goes through the same mechanism as through a regular interface, but the compiler often optimises it away. When the generic function is instantiated for a concrete type, calls like item.Total() may be compiled as direct calls rather than interface dispatch. This depends on Go's gc compiler internals and is not something you should rely on, but it's good to know there's no inherent runtime tax over an equivalent non-generic version.

The following variation uses a pointer method on the type parameter:

Because Inc and Value are pointer methods, only *Cart satisfies Counter. The slice has element type *Cart. Inside TickAll, item has type *Cart (instantiated from T), and the pointer method calls modify the underlying value. The same mutation rules you'd expect from non-generic code apply.

Where Constraints Live

You can put a constraint in two places: declared inline at the call site, or named at the package level. Both are legal Go. The choice is mostly about reuse and readability.

Inline at the call site

For one-off constraints that aren't reused, declare the interface inline inside the type parameter list:

The constraint interface{ Price() float64 } is anonymous: it has no name and lives only in this one function signature. Good for tiny, one-shot helpers where naming the interface would feel like overkill.

Named at the package level

When two or more generic functions share the same constraint, name it:

Priceable is declared once and reused by both functions. If a third function comes along that also needs Price(), it gets the same constraint. If the contract grows ("now we also need a SKU() method"), you change the interface in one place.

There's a deeper benefit. A named, package-level interface can be used both as a constraint and as a regular runtime interface. You can write var prices []Priceable somewhere else in the package, and the same interface that gates generic functions also lets you build a heterogeneous slice. An anonymous inline constraint can't be reused that way; it has no name to refer to.

The practical guideline is straightforward:

SituationWhere to put the constraint
Used by one function, tiny, unlikely to growInline interface{ ... }
Used by two or more generic functionsNamed at package level
Will also be used as a runtime interfaceNamed at package level
Standard library already defines a fitting one (fmt.Stringer, io.Reader, error)Reuse the standard one

One small style note. Some teams prefix constraint-only interfaces with Constraint (PriceableConstraint) or put them in a constraints subpackage to make the intent obvious. That's a project style choice. The Go standard library doesn't draw such a distinction. The standard library's slices and maps packages, for example, use the same comparable constraint that anyone writing a map-key generic function would use.

Forward Teaser

Constraints aren't limited to methods. They can also describe sets of underlying types directly using union types and the ~ operator, which is what lets you write generic numeric code with +, -, <, and friends. The next chapter covers that.

Quiz

Type Constraints Quiz

10 quizzes