1. Why constraints are needed
When you first see func F[T any](...), it can feel like any is just decoration: “well, let it be any type.” In practice, a constraint is not a decorative bracket at all, but a permission for operations inside generic code. A constraint answers two questions at once: which types you are allowed to substitute for T, and what exactly you are allowed to do with values of type T inside the function body. And this is precisely what makes generic code in Go “statically typed,” rather than “we will sort it out at runtime somehow.”
In the language, this is formalized through the idea of a type set: a constraint describes the set of permitted types for a type parameter. This is clearly visible in the official explanations about type parameters and constraints: the type parameter “ranges over” the types allowed by the constraint.
You can imagine it like this (very schematically):
flowchart TD A["T — type parameter"] --> B["constraint"] B --> C["set of allowed types"] C --> D["which operations you can do with T inside the function"]
2. any: “any type,” but not “any operator”
any in Go is a predefined alias for the empty interface (historically interface{}), meaning “any type fits.” But the key point is: T any gives you no guarantees about operations. It says only one thing: “you can substitute anything.” And since it can be “anything,” inside the function you cannot, for example, safely use == or +, because not all types support those operations.
Think of it this way: any is a “box of any size,” but without instructions on “what to do with what’s inside.” You can carry the box, move it around, return it from a function, print it “as is” via fmt, but you cannot require the contents to be able to, say, “multiply” or “compare.”
What you can usually do with T any
The main advantage of any: it is ideal for functions that do not perform operations on the value, but only move it around: return it, put it into a struct, swap it, pick the first element, and so on.
Example: “swap two values of any type.”
package main
import "fmt"
func Swap[T any](a, b T) (T, T) {
return b, a
}
func main() {
x, y := Swap(10, 20)
fmt.Println(x, y) // 20 10
}
Here there is no comparison and no arithmetic — we simply rearrange the values.
Another example: “safely take the first element, or say that there isn’t one.” Notice the idiom var zero T: this is how we get the zero value for any T.
package main
import "fmt"
func First[T any](xs []T) (T, bool) {
if len(xs) == 0 {
var zero T
return zero, false
}
return xs[0], true
}
func main() {
v, ok := First([]string{"go"})
fmt.Println(v, ok) // go true
}
You will see this (T, bool) pattern many times in Go code: it makes “absence of a value” part of the contract rather than an unexpected surprise.
What you cannot do with T any
The most common trap: “if it’s any, then I can compare two T values with ==.” You cannot. Because not all types are comparable.
Pseudo-example (this code will not compile):
package main
func EqualBad[T any](a, b T) bool {
return a == b // compile error: invalid operation: a == b (not comparable)
}
func main() {}
And this is good news: the error happens at compile time, not “sometime later for a user on Friday evening.”
3. comparable: equality and map keys
When you truly need to compare values, any no longer fits: you need a constraint that guarantees the comparison operation is defined. For that, Go provides the predefined constraint comparable.
In official materials, comparable is described as a special constraint for types where == and != are defined. This is tightly connected to which types can be used as map keys: map keys must be comparable.
Mini-table: any vs comparable
Before writing code, it helps to keep a simple table in mind:
| Constraint | “Who fits?” | What you can do inside generic code | Typical use case |
|---|---|---|---|
|
any type | almost nothing “type-specific”; mostly moving/storing/returning | “return the first element,” “wrap in a container,” “swap” |
|
types with ==/!= | you can compare a == b, store as a map[T]... key | Contains, Set, deduplication, map indexes |
Example: Contains for any comparable types
A classic: searching for a value in a slice using ==. Here T comparable is the minimally sufficient constraint.
package main
import "fmt"
func Contains[T comparable](xs []T, v T) bool {
for _, x := range xs {
if x == v {
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]int{1, 2, 3}, 2)) // true
fmt.Println(Contains([]string{"a", "b"}, "c")) // false
}
If we had written T any, the compiler would not allow using ==.
Why map requires comparable
map[K]V in Go works by comparing and hashing keys. Therefore, the key must be comparable. In generic code, this shows up very literally: if you want to use K as a map key, the constraint on K must be comparable; otherwise the compiler will tell you that you are asking for the impossible. You can also see this in examples about data structures where attempting to store elements in map[E]... leads to the message “missing comparable constraint”.
4. Go 1.25 nuance: comparable and interfaces
Comparison in Go looks “safe” until you run into interfaces. Historically, in Go you can have map[any]string, and it compiles. But if you try to use as a key a value that is not comparable by itself (for example, a slice), you will get a runtime panic. Official text even illustrates this with an example of trying to put []int{} into a map[any]... and getting a runtime error.
Why does this matter for generics specifically? Because in modern Go versions (in particular, starting with Go 1.20), the comparable constraint was “expanded” to work better with interface types like any (as a type argument), and some things that did not compile before now do compile. But the logic remains honest: if an interface contains a non-comparable value, comparison can panic, exactly as it can in ordinary non-generic code.
A practical rule for a beginner is simple: if your T comparable is “normal” types (numbers, strings, structs without slices/maps, etc.), you are fine. If you start comparing “boxes with unknown contents” (like values of type any), remember: the compiler allowed ==, but runtime can still say “oops.”
5. How to choose: any or comparable
When you start writing generics, it is very tempting to make things “universal”: put any everywhere, and then “we will compare somehow.” But in Go this does not work precisely because Go tries to make errors visible earlier. So the strategy is usually: start with the softest constraint (any) and strengthen it only when the operation is truly needed.
If a function stores, returns, selects, or swaps, any is often enough. If a function compares (==), builds a map by keys, makes a set/unique, then you need comparable. This is the “minimally sufficient constraint”: it makes the code applicable to the maximum number of types while still allowing the operations you need.
6. Example: generic utilities for a task app
Now we will make this as down-to-earth as possible: imagine we have a small console “task list” app. It is not “perfect” yet, but what matters is something else: we want examples that are not abstract “apples and oranges,” but that actually help you write code.
Let a task have a title and tags:
package main
type Task struct {
Title string
Tags []string
}
Deduplication: Unique[T comparable]
A typical case: we want to collect all tags from tasks and remove duplicates. Removing duplicates almost always relies on map, which means the element must be comparable.
package main
func Unique[T comparable](xs []T) []T {
seen := make(map[T]struct{}, len(xs))
out := make([]T, 0, len(xs))
for _, x := range xs {
if _, ok := seen[x]; ok {
continue
}
seen[x] = struct{}{}
out = append(out, x)
}
return out
}
Here T comparable is not “because it looks nice,” but because map[T]struct{} would otherwise be impossible.
Collect task tags and make them unique
Now we use Unique in an application-level function.
package main
func AllTags(tasks []Task) []string {
var tags []string
for _, t := range tasks {
tags = append(tags, t.Tags...)
}
return Unique(tags)
}
Notice: generics give exactly what we need here. We write Unique once, and then use it for []string, []int, []TaskID — for anything comparable.
Counting frequency: CountBy[T comparable]
Another useful pattern is counting how many times each element occurs. And again: a map key must be comparable, so T comparable.
package main
func CountBy[T comparable](xs []T) map[T]int {
m := make(map[T]int, len(xs))
for _, x := range xs {
m[x]++
}
return m
}
This lets you count frequencies of tags, statuses, authors — anything, as long as it is comparable.
Where any fits: GroupBy[T any, K comparable]
Sometimes you want to group items by a key: tasks by tag, users by city, files by extension. This shows very clearly how different constraints work together: we do not compare the elements themselves, so T any; but we put the group key into a map, so K comparable.
package main
func GroupBy[T any, K comparable](items []T, keyFn func(T) K) map[K][]T {
m := make(map[K][]T)
for _, it := range items {
k := keyFn(it)
m[k] = append(m[k], it)
}
return m
}
This is a great “mental anchor”: any for what you only move around, and comparable for what you place into map keys.
How to read standard library signatures without fear
When you start looking at standard packages that use generics, you will see signatures like Clone[M ~map[K]V, K comparable, V any](m M) M. In this lecture, we do not need the entire signature (there are nuances there that we are not covering right now); what matters is that it reads almost like plain English: the key K must be comparable, and the value V can be any. This reflects the nature of map: the key must support equality, the value can be anything.
If you learned to spot “K comparable” and immediately understand “aha, the keys are comparable,” you are already reading 70% of practical generic code without unnecessary pain.
7. Common mistakes when working with any and comparable
Mistake #1: use T any and try to do ==.
This is one of the most common situations: you write a “universal” function, put any, and then suddenly you want to compare a and b. The compiler will not allow it, and that is correct: any does not guarantee that the type supports equality. The fix is simple: either change the constraint to comparable, or accept a comparison function (but that is a different technique).
Mistake #2: try to make map[T]... where T any.
It looks logical: “well, T is any, so the key can be any.” But map requires the key to be comparable, and generic code must express that explicitly. If you want map[T]int, then T must be comparable, otherwise you are asking the compiler to build a map over types that fundamentally cannot be keys.
Mistake #3: add comparable “just in case” where it is not needed.
This is the opposite extreme. Sometimes a function does not compare values at all and does not use them as keys, but the author adds comparable just because “it’s safer.” In reality, this worsens the API: you forbid using the function for non-comparable types (for example, []byte), even though you could work with them just fine by keeping any.
Mistake #4: assume comparable guarantees no panic in 100% of cases.
In most application cases, that is almost true (and that is why comparable is so convenient). But if you work with interface values (for example, a key of type any, which may contain a non-comparable dynamic type), comparison or using it in a map can cause a runtime panic — exactly as in ordinary code without generics. It is useful to remember this boundary so that you are not surprised later by the phrase “hash of unhashable type.”
Mistake #5: confuse “the type any” with “a type parameter T constrained by any”.
Visually they look the same, but the meaning is different. any as a concrete type is an interface value that can hold different dynamic types. T any is a type parameter that, for a particular instantiation, becomes a concrete type. This confusion is often why people expect T any to behave “like interface{},” when in fact it is a statically substituted type.
GO TO FULL VERSION