Back to blog

Sunday, October 25, 2026

Why Do Go Generics Feel Like A Type Puzzle?

Title card for 'Why Do Go Generics Feel Like A Type Puzzle?' with a short code example

Introduction

Go generics feel like a type puzzle because generic code is written against a set of types, not against one type, so the reader must hold four pieces together at once: the type parameter, its constraint, the type set that constraint allows, and the type argument the compiler inferred for a given call. The compiler checks every operation in the generic body against the constraint alone, and it infers type arguments only from function arguments, never from the result, so a line that reads clearly for one concrete type can fail for a reason that lives in a different part of the declaration.

Before You Start

  • Type parameter: a placeholder for a type, written in square brackets, for example T in func Max[T Ordered](a, b T) T. Inside the function body, T stands for one type, chosen by the caller.
  • Type argument: the concrete type supplied for a type parameter at a call site, for example int in Max[int](2, 9). A call with no type arguments asks the compiler to infer them.
  • Constraint: an interface that limits which type arguments a type parameter accepts, and that decides which operations the generic body may use. A constraint is a contract, not a comment.
  • Type set: the set of types a constraint allows. A union such as int64 | float64 allows exactly those two types. A method set constraint, such as an interface with a String() string method, allows any type with that method.
  • Instantiation: the act of replacing type parameters with type arguments, producing a concrete function or type, for example Max[int] from Max[T Ordered].
  • Type inference: the compiler working out type arguments from the types of function arguments, so the caller can omit them. Inference is a convenience, not a separate feature, and it has fixed limits.
  • any: an alias for the empty interface interface{}, allowing every type. A value of a type parameter constrained by any supports almost no operations, because the set includes types that support none of them.
  • comparable: a predeclared constraint allowing types whose values may be compared with == and !=. It is required for map keys, and it does not imply ordering, so it does not allow < or >.
  • Underlying type: the type a named type is built from, reached with ~ in a constraint, for example ~int allows int and any named type whose underlying type is int, such as type Score int.
  • Ordered: in this article, a small named constraint for types that support < and >, built from ~int, ~int64, ~float64, and ~string. The standard library had no ordered constraint in Go 1.20 (Go 1.21 added cmp.Ordered), so the article defines its own.

Prerequisites

  • Go 1.20 or later, because generics arrived in Go 1.18 and this article stays inside the Go 1.20 language rules.
  • A terminal, and a text editor.
  • No third party packages are required for the complete example.
go version

Requires: Go 1.20 or later.

Project Layout

generics-puzzle/
├── go.mod        # module declaration, Go version
├── main.go       # generic functions, generic types, and main function
└── main_test.go  # tests for the generic functions and types
  • go.mod: declares the module and the minimum Go version.
  • main.go: holds the generic code, so each puzzle piece can be read in declaration order.
  • main_test.go: holds tests that check the generic functions and types with concrete type arguments.

A Type Parameter Is A Placeholder, Not A Type

A generic function declaration has two parameter lists, and confusing them is the first puzzle piece. The first list, in square brackets, declares type parameters. The second list, in parentheses, declares ordinary value parameters. The type parameters say which types the function may work with. The value parameters say which values it receives.

func Max[T Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Inside Max, T is one type, not all types at once. At a call such as Max(3, 7), T is int for that whole call. At a call such as Max("apple", "banana"), T is string for that whole call. A single call never mixes types for one type parameter, because one parameter stands for one type per instantiation. A call such as Max(1, "two") cannot unify int and string into one T, and the compiler reports that the types do not match.

A generic type works the same way, with one extra rule that surprises readers: a generic type must always be instantiated before use. A variable cannot be declared as a bare generic type, because the compiler needs to know which concrete type it is.

type Pair[K comparable, V any] struct {
    Key   K
    Value V
}

Pair[string, int] is a concrete type. Pair on its own is not, and a declaration such as var p Pair is an error, because no type arguments were supplied and no value exists from which to infer them. A constructor function can hide that explicit list only when its arguments carry the types, as the complete example shows for a store built from no arguments, where the caller must still write the type arguments.

Two instantiations of one generic type are different types. Pair[string, int] and Pair[string, string] share source code, not identity, and a value of one cannot be assigned to a variable of the other. The same holds for generic functions: Max[int] and Max[string] are separate instantiations, each checked against the constraint with its own type argument.

The Constraint Is The Only Contract The Body May Use

The second puzzle piece is that the generic body may use only operations the constraint permits, even when the reader knows the concrete type at a call site will support more. The compiler checks the body once, against the constraint, not once per call. If the constraint does not permit an operation, the body does not compile, no matter how safe that operation would be for the types actually passed.

func Add[T any](a, b T) T {
    return a + b
}

This function does not compile, because any allows every type, including types that do not support +, so the compiler cannot permit + on a value of type T. The fix is not to add a type assertion inside the body, but to narrow the constraint to a set whose members all support +, such as a union of numeric types. The constraint, not the call site, decides what the body may do.

The same rule explains comparable and ordering. A type parameter constrained by comparable may be compared with == and !=, and may be used as a map key, because every comparable type supports those operations. It may not be ordered with < or >, because comparability does not imply ordering: structs and pointers are comparable, but they do not support <. A function that needs ordering needs an ordered constraint, and a function that needs a map key needs a comparable constraint. A constraint that needs both must say both, as the complete example does with a constraint that embeds comparable and lists ordered types.

A constraint written as an interface with methods works differently from a constraint written as a union. A method constraint allows any type with those methods, and the body may call those methods. A union constraint allows only the listed types, and the body may use operations common to all of them, such as + for a numeric union. Mixing the two ideas, for example expecting a union constraint to allow a method call, or a method constraint to allow arithmetic, is a common source of confusion, because the compiler follows the constraint as written, not the intent behind it.

Type Sets, Unions, And The Tilde

A type set is the set of types a constraint allows, and three details of type sets cause most of the remaining confusion. The first detail is the union operator |, which lists allowed types, so int64 | float64 allows exactly those two types, and no other. The second detail is the tilde ~, which widens a listed type to every named type with the same underlying type, so ~int allows int and also type Score int, while plain int allows only int itself. A named type that fails a constraint is often fixed by adding ~ to the constraint, not by changing the named type.

The third detail is the split between basic interfaces and general interfaces. A basic interface lists only methods, and it may be used as an ordinary type, for example as a function parameter or a variable type. A general interface also lists type elements, such as a union, and it may be used only as a constraint, not as an ordinary type. A constraint such as Number with a union inside it therefore cannot be declared as a variable type, and code that tries to do so fails with a message about general interfaces. That split is deliberate: a type set is a compile time contract, not a runtime value type.

A related rule is that type sets in one union must not overlap in a way the compiler cannot separate, and a type parameter itself cannot appear as a term in its own constraint set. Those rules rarely appear in small examples, but they explain why some constraint declarations that look reasonable are rejected, and why the fix is usually to restructure the constraint into smaller named pieces rather than to widen one large union.

Inference Successes: What The Compiler Can Work Out

Inference succeeds when every type parameter appears in a function argument in a way the compiler can read off. The tutorial rule is direct: the compiler infers type arguments from the types of function arguments. A function that takes a slice of T infers T from the slice element type. A function that takes a map of K to V infers both K and V from the map type. A function that takes two arguments of the same type parameter infers that parameter only when both arguments agree on one type.

func First[T any](items []T) (T, bool) {
    if len(items) == 0 {
        var zero T
        return zero, false
    }
    return items[0], true
}

first := First([]int{10, 20, 30})

Here T is int, inferred from the slice argument, and the caller writes no type arguments. A partial list is also permitted for functions: a caller may specify the leading type arguments and let the compiler infer the rest from the arguments, though in practice most calls either specify all of them or none of them, because a partial list still requires the reader to track which parameters were given and which were inferred.

Inference also works through structure. A parameter of type []T infers T from the element type of the argument slice. A parameter of type map[K]V infers K and V from the key and value types of the argument map. A parameter that is itself a generic type, such as a Pair[K, V] argument, infers K and V from the instantiation of that argument. In each case, the compiler reads the type argument from a value that is actually passed, which is why inference feels natural in data driven helpers and fragile everywhere else.

Inference Failures: What The Compiler Cannot Work Out

Inference fails in four clear cases, and each one has a different fix, which is why the failures feel puzzling when they are treated as one error.

The first case is a type parameter that appears only in the return type. A function such as Zero[T any]() T takes no arguments, so no argument carries T, and the compiler reports that it cannot infer T. The destination of the result does not help: an assignment such as var x int = Zero() still fails, because inference never uses the destination of a call result. The fix is explicit instantiation, Zero[int](), which supplies the missing type argument directly.

The second case is conflicting arguments. A call such as Max(1, "two") gives the compiler two different types for one parameter, and unification fails, because one parameter stands for one type per call. The fix is to make the arguments agree, or to call with an explicit type argument and convert one argument to that type, so the call has a single consistent type.

The third case is a generic type, rather than a generic function. Type arguments for a generic type are never inferred from a composite literal or a variable declaration, so Pair{Key: "a", Value: 1} without type arguments is an error, while Pair[string, int]{Key: "a", Value: 1} is correct. A constructor function can recover inference only when it takes arguments that carry the types; a constructor with no value arguments, such as a store constructor, cannot infer anything, and its caller must write the type arguments.

The fourth case is a generic function used as a value without being called. A generic function must be instantiated, explicitly or by a call, before it is used as a value, because an uninstantiated generic function has no concrete type to assign. The fix is to instantiate it first, for example by writing the type arguments, or by wrapping it in a concrete function that calls it.

When inference fails, the error message names the parameter it could not infer, and the fastest reading is to ask which argument should have carried that parameter, and whether that argument exists at all. If no argument carries it, write it explicitly. If two arguments disagree, make them agree. If the failing item is a type rather than a function call, write the type arguments in full.

Methods Could Not Add Type Parameters Before Go 1.27

Under the Go 1.20 language rules used by this article, and in every release before Go 1.27, a method may use the type parameters of its receiver type, but it may not declare type parameters of its own. A method on Store[K, V] may use K and V, because those belong to the receiver, but it may not introduce a new parameter such as U for a conversion result. Code that tries to write such a method fails, and the fix is to move the operation to a package level generic function, which may declare its own type parameters.

func (s *Store[K, V]) Get(key K) (V, bool) {
    value, ok := s.data[key]
    return value, ok
}

Get uses K and V from the receiver, and declares no new parameters, so it compiles. A conversion method that would need a new result type cannot be written as a method under these rules, and the article therefore provides conversions and transformations as package level functions, with their own type parameter lists. That placement is a language rule, not a style preference, and it explains why generic container libraries in Go 1.20 expose many helpers as functions rather than methods.

A related restriction is that an interface method also cannot declare its own type parameters under these rules, so a generic operation cannot be expressed as an interface method that varies its result type per call. The workaround is the same: keep the varying part in a generic function, and keep interface methods to the fixed signature the interface promises.

For why Go has no built in generic map and filter, see Why Does Go Have No Built In Generic Map And Filter?.

Go 1.27, released in August 2026, lifts this rule for concrete methods: a method may now declare its own type parameters, as in func (List[E]) Map[R any](f func(E) R) List[R]. Interface methods still cannot have type parameters, and a generic method cannot satisfy an interface method. If your go.mod says go 1.27 or later, a method with its own type parameter is an option; on older versions, keep the package level function. See Go 1.27 Release Notes and Generic Methods.

How To Simplify A Generic Declaration

Simplification starts from the concrete case, not from the generic one. Write the function for one concrete type first, and check that it compiles and does what it should. Then replace the concrete type with a type parameter, and ask what constraint that one type satisfied, and what operations the body actually uses. The smallest constraint that permits exactly those operations is the right constraint, and a constraint written from the body, rather than from a guess about future callers, stays small.

Name a constraint when it is used twice, or when its meaning is not clear from the union alone. A name such as Ordered or OrderedKey tells the reader what the set is for, while a repeated union forces the reader to compare two lists character by character. Name a constraint at the point where its purpose becomes clear, not before, because a named constraint for a single use adds a name the reader must resolve without adding clarity.

Keep the number of type parameters low. Each additional parameter is another type the reader must track, another inference source to check, and another place where two arguments can disagree. Two parameters, such as a key and a value, are common and readable. Three or more parameters usually signal that the function is doing more than one job, or that an intermediate type should be named, so the function can be split or the types can be grouped.

When inference fails, prefer an explicit type argument at the call site over restructuring the function, because an explicit argument is local and visible, while a restructured signature changes every caller. When a generic type must be instantiated, prefer a constructor that takes at least one argument carrying the types, so callers can rely on inference, and reserve explicit instantiation for constructors with no value arguments.

Design Decisions And Tradeoffs

OptionUse whenCost
Concrete types firstThe logic is new, or the constraint is unclearOne function per type, until the shared shape is clear
One small named constraintThe same set is used twice, or its purpose needs a nameA name the reader must resolve, and a place to keep the set in sync
Inline union constraintThe set is used once, and is shortRepetition when the set is reused, and harder comparison across uses
anyThe body needs no operations on the value, such as storing or printing itAlmost no operations are permitted, so arithmetic, comparison, and ordering all fail
comparableEquality, map keys, or deduplication are neededNo ordering, so < and > still fail
Ordered constraintComparison with < and > is neededOnly ordered types qualify, so structs and slices are excluded
Explicit type argumentsInference cannot work out a parameter, or the call must be clearMore text at the call site, and a list the reader must check against the declaration
Inferred type argumentsEvery parameter appears in a function argumentThe reader must work out the inferred types, and a change to an argument can change the instantiation silently
Generic typeA container or pair must carry its element types in its own typeEvery use needs full instantiation, and two instantiations are different types
Generic functionAn operation must work across types without a containerInference limits apply, and a return only parameter cannot be inferred
Method on a generic typeThe operation uses only the receiver type parametersBefore Go 1.27, no new type parameters may be added by the method
Package level generic functionThe operation needs a new type parameter, such as a conversion resultThe operation sits outside the type, so method style chaining is not available

A design in this article has a deliberate compromise, stated openly. The complete example uses one small ordered constraint and one store type, not a full container library, so the inference and method rules can be read without a large API in the way. In a real library, the same rules apply at larger scale, and the cost of a wide constraint or a long parameter list grows with every caller. The simplification rules in the previous section are not compromised by the small example: they are the same rules, applied to a smaller surface.

Complete Example

The complete example is a small generic toolkit. A maximum function works on ordered types. A first element function works on any slice element type. A pair type carries a comparable key and any value. A store type holds a map, with methods that use only the receiver parameters. A sorted keys function works on a map whose keys are both comparable and ordered. A zero function shows the return only case, used with an explicit type argument in the test.

// go.mod
module genericspuzzle

go 1.20
// main.go
package main

import (
    "fmt"
    "sort"
)

type Ordered interface {
    ~int | ~int64 | ~float64 | ~string
}

type OrderedKey interface {
    comparable
    ~int | ~int64 | ~float64 | ~string
}

func Max[T Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

func Zero[T any]() T {
    var z T
    return z
}

func First[T any](items []T) (T, bool) {
    if len(items) == 0 {
        var zero T
        return zero, false
    }
    return items[0], true
}

type Pair[K comparable, V any] struct {
    Key   K
    Value V
}

func (p Pair[K, V]) Describe() string {
    return fmt.Sprintf("%v=%v", p.Key, p.Value)
}

type Store[K comparable, V any] struct {
    data map[K]V
}

func NewStore[K comparable, V any]() *Store[K, V] {
    return &Store[K, V]{data: make(map[K]V)}
}

func (s *Store[K, V]) Set(key K, value V) {
    s.data[key] = value
}

func (s *Store[K, V]) Get(key K) (V, bool) {
    value, ok := s.data[key]
    return value, ok
}

func SortedKeys[K OrderedKey, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    sort.Slice(keys, func(i, j int) bool {
        return keys[i] < keys[j]
    })
    return keys
}

func main() {
    fmt.Println(Max(3, 7))
    fmt.Println(Max[int](2, 9))
    value, ok := First([]int{10, 20, 30})
    fmt.Println(value, ok)
    pair := Pair[string, int]{Key: "age", Value: 30}
    fmt.Println(pair.Describe())
    store := NewStore[string, int]()
    store.Set("age", 42)
    stored, found := store.Get("age")
    fmt.Println(stored, found)
    fmt.Println(SortedKeys(map[string]int{"banana": 2, "apple": 1}))
}

Commands to run it, from the generics-puzzle folder:

go run .

Expected output:

7
9
10 true
age=30
42 true
[apple banana]

Why this program reads the way it does, in order:

  • Max(3, 7) infers T as int from the arguments, while Max[int](2, 9) states the same type explicitly, so the two calls show inference and explicit instantiation side by side.
  • First([]int{10, 20, 30}) infers T as int from the slice element type, and returns the first element with a found flag, so an empty slice has a defined result.
  • Pair[string, int] is a full instantiation, because a generic type cannot be used without type arguments, and its Describe method uses only the receiver parameters.
  • NewStore[string, int]() is a full instantiation for the opposite reason to First: the constructor takes no value arguments, so no argument carries K or V, and inference has nothing to work from.
  • SortedKeys infers K as string and V as int from the map argument, and its OrderedKey constraint permits both map key use and < comparison, because it embeds comparable and lists ordered types.

Testing Generics

Test a generic function with more than one instantiation, because a test with one type proves the logic for that type only, not the constraint. A maximum test with integers and a maximum test with strings check the same body under two type arguments, and a store test with string keys and integer values checks the key and value parameters together.

// main_test.go
package main

import (
    "testing"
)

func TestMax(t *testing.T) {
    if got := Max(3, 7); got != 7 {
        t.Errorf("Max(3, 7) = %d, want 7", got)
    }
    if got := Max("apple", "banana"); got != "banana" {
        t.Errorf("Max apple banana = %q, want banana", got)
    }
    if got := Max[int](2, 9); got != 9 {
        t.Errorf("Max[int](2, 9) = %d, want 9", got)
    }
}

func TestZeroExplicit(t *testing.T) {
    if got := Zero[int](); got != 0 {
        t.Errorf("Zero[int]() = %d, want 0", got)
    }
    if got := Zero[string](); got != "" {
        t.Errorf("Zero[string]() = %q, want empty string", got)
    }
}

func TestFirst(t *testing.T) {
    value, ok := First([]int{10, 20, 30})
    if !ok || value != 10 {
        t.Errorf("First = %d %v, want 10 true", value, ok)
    }
    _, ok = First([]int{})
    if ok {
        t.Errorf("First on empty slice ok = true, want false")
    }
}

func TestStore(t *testing.T) {
    store := NewStore[string, int]()
    store.Set("age", 42)
    got, ok := store.Get("age")
    if !ok || got != 42 {
        t.Errorf("Store Get = %d %v, want 42 true", got, ok)
    }
    _, ok = store.Get("missing")
    if ok {
        t.Errorf("Store Get missing ok = true, want false")
    }
}

func TestSortedKeys(t *testing.T) {
    got := SortedKeys(map[string]int{"banana": 2, "apple": 1})
    want := []string{"apple", "banana"}
    if len(got) != len(want) {
        t.Fatalf("SortedKeys length = %d, want %d", len(got), len(want))
    }
    for i, w := range want {
        if got[i] != w {
            t.Errorf("SortedKeys[%d] = %q, want %q", i, got[i], w)
        }
    }
}

func TestPairDescribe(t *testing.T) {
    pair := Pair[string, int]{Key: "age", Value: 30}
    if got := pair.Describe(); got != "age=30" {
        t.Errorf("Pair Describe = %q, want age=30", got)
    }
}

Commands to run the tests and the static check:

go vet ./...
go test ./...

Expected output for the tests is a pass for every test, including the explicit Zero tests, because those calls supply the type argument inference cannot work out. go vet reports suspicious constructs. It does not prove that a constraint is the right one, so do not treat a clean vet run as proof that a generic declaration is readable. The tests with two instantiations are the proof for this example that the body works for more than one type argument.

Common Mistakes

  • Using an operation the constraint does not permit: a body that adds values of a type parameter constrained by any, or orders values constrained by comparable, fails even when the concrete call would support the operation. The fix is to narrow the constraint to a set whose members all support the operation.
  • Expecting comparability to imply ordering: a comparable type may be compared with ==, but ordering needs a separate ordered constraint, because structs and pointers are comparable without supporting <. The fix is a constraint that lists ordered types, or that embeds comparable alongside ordered types when both are needed.
  • Forgetting the tilde for named types: a constraint of plain int rejects a named type such as type Score int, because plain int allows only int itself. The fix is ~int in the constraint, which allows any named type with underlying type int.
  • Using a general interface as an ordinary type: a constraint interface with a union inside it may be used only as a constraint, not as a variable or parameter type. The fix is to use the constraint in a type parameter list, and to use a basic interface with methods only when an ordinary interface type is needed.
  • Calling a return only generic function without type arguments: a function such as Zero[T any]() T has no argument carrying T, and the result destination does not supply it, so the call fails. The fix is an explicit type argument, such as Zero[int]().
  • Mixing types for one type parameter: a call with arguments of two different types for one parameter cannot unify them into one type argument. The fix is to make the arguments agree, or to convert one argument to the explicit type argument used in the call.
  • Using a generic type without instantiation: a declaration such as var p Pair fails, because a generic type needs full type arguments, and no literal or declaration infers them. The fix is a full instantiation, such as Pair[string, int], or a constructor whose arguments carry the types.
  • Adding type parameters to a method: before Go 1.27, a method may use only the receiver type parameters, and a method that needs a new result type fails. The fix is a package level generic function with its own type parameter list, or a generic method on Go 1.27 or later.
  • Writing one large constraint for every future caller: a wide union or a long parameter list forces every reader to check every member, and every new caller to satisfy all of it. The fix is the smallest constraint the body actually uses, named when it is reused.
  • Relying on inference to document a call: an inferred call hides the instantiation, so a reader must work out the types from the arguments, and a change to an argument can change the instantiation without changing the call text. The fix is an explicit type argument at call sites where the inferred type is not clear from the surrounding code.
  • Treating two instantiations as one type: values of Pair[string, int] and Pair[string, string] cannot be assigned to each other, because they are different types that share source code. The fix is to convert or rebuild the value under the target instantiation, not to assign it directly.

Production Notes

  • Readability: a generic declaration is read by every future maintainer, not only by its author, so a constraint that needs a comment to explain it is usually too wide, and a call that needs a comment to state its inferred types usually needs explicit type arguments.
  • API stability: a constraint is part of a public API, because widening it can admit types the body cannot handle, and narrowing it can break callers that relied on the wider set. Change a constraint deliberately, with tests under more than one instantiation, rather than as a side effect of fixing one call.
  • Error messages: a constraint failure names the type argument and the constraint it does not satisfy, so the fastest fix is to read the constraint, not the body. A body failure names the operation and the constraint, so the fastest fix is to read the body against the constraint, not the call site.
  • Testing: a generic function tested with one type argument is tested for one instantiation only. Test with at least two types, including a named type when the constraint uses ~, because a named type is the case plain union members miss.
  • Deployment: the example needs Go 1.20 or later. Pin that version in go.mod, run go vet ./... and go test ./... in continuous integration, and do not deploy a change that fails the tests under more than one instantiation. A vet pass alone is not enough for a generic declaration, because vet does not prove that a constraint is readable or minimal.

Decision Guide

  • When the body uses no operations on the value: use any, and accept that the body may only store, pass, or print the value.
  • When equality or map keys are needed: use comparable, and do not expect ordering from it.
  • When ordering is needed: use a named ordered constraint, and add comparable to it when the same type must also be a map key.
  • When a named type must qualify: use ~ in the constraint, so types with the same underlying type are allowed.
  • When a constraint is used twice: name it, so the set is written once and read once.
  • When inference has no argument to work from: write the type argument explicitly, at the call or at the type instantiation.
  • When a method needs a new type parameter: before Go 1.27, move the operation to a package level generic function; on Go 1.27 or later, a generic method also works.
  • When a declaration has three or more type parameters: split the operation, or group the types into a named generic type, so each declaration stays readable.
  • When the concrete logic is unclear: write the concrete version first, then generalise from the operations the body actually uses.
  • When the generic version is harder to read than two concrete versions: keep the concrete versions, because generics are a tool for shared shape, not a requirement to remove every repetition.

Conclusion

Generics feel like a type puzzle because the puzzle has four moving parts, and the compiler checks them in an order the reader does not always follow: the constraint decides what the body may do, the type set decides which types qualify, inference reads type arguments only from function arguments, and instantiation turns the generic declaration into a concrete type or function. Once those parts are separated, most generic errors read as one of four statements: the constraint does not permit the operation, the type does not satisfy the constraint, inference has no argument carrying the parameter, or the type was never instantiated.

Summary Of Key Points

  • Constraint decides operations: the generic body may use only operations every type in the constraint set supports.
  • any allows almost nothing: every type qualifies, so arithmetic, comparison, and ordering are all unavailable in the body.
  • comparable is not ordered: equality and map keys work, < and > need a separate ordered constraint.
  • Tilde widens to underlying types: ~int allows named types built on int, while plain int allows only int.
  • Inference reads arguments only: a return only parameter, a conflicting pair of arguments, or a generic type literal cannot be inferred, and needs an explicit type argument.
  • Generic types need full instantiation: a bare generic type is not a type, and two instantiations are different types.
  • Methods use receiver parameters only: before Go 1.27, a new result type belongs in a package level generic function; Go 1.27 added generic methods.
  • Simplify from concrete code: write one concrete version first, name a constraint when it is reused, and keep parameter lists short.

Follow those rules, and a generic declaration stops being a puzzle to solve on every read: the constraint states the contract, the call states the types when inference cannot, and the body uses only what the contract permits.

Additional Resources

References

  1. Tutorial: Getting started with generics. Go team, Go Documentation. https://go.dev/doc/tutorial/generics. Accessed 2026-10-25.
  2. The Go Programming Language Specification, Type parameters. Go team, Go Documentation. https://go.dev/ref/spec#Type_parameters. Accessed 2026-10-25.
  3. builtin package, comparable. Go team, Go Packages. https://pkg.go.dev/builtin#comparable. Accessed 2026-10-25.
  4. builtin package, any. Go team, Go Packages. https://pkg.go.dev/builtin#any. Accessed 2026-10-25.