Proposal Details

Background

Go 1.18 introduced type parameters on functions and types, but methods cannot declare their own type parameters. Attempting to do so yields:

method must have no type parameters

This limitation forces APIs to either:

  • push additional type parameters up to the receiver type, or

  • move method logic to free functions (less discoverable/ergonomic), or

  • use any + manual assertions (unsafe/verbose).

Proposal

Permit type parameters on methods (“generic methods”). Syntax mirrors generic functions:

type Query[T any] struct { /* ... */ }

// New: method-level type parameters
func (q *Query[T]) Include[F any](selector func(*T) *F) *Query[T] {
    // ...
    return q
}

Method declarations may include a type parameter list after the receiver, before the parameter list. Constraints and inference rules follow existing function generics.

Motivation / Use cases

  1. Fluent, type-safe builders go user, err := repo.Users(). FindOne(cond). Include(func(u *User) *[]Role { return &u.RoleList }). Run(ctx)

    Include depends on the receiver element type T but also introduces a new related type F (the field type). Today this must be a free function:

    go // workaround today q = Include(q, func(*User) *[]Role { ... })

    which harms discoverability and chaining.

  2. APIs that quantify over a method-local type

    • Serialization hooks: EncodeAs[F ~[]byte|~string]() for alternative wire types

    • Collection helpers: Map[U any](fn func(T) U) []U as a method on a generic container

    • Database relations: Join[F any](...), Select[Col any](...)

  3. Keeping receiver generic surface minimal

    Forcing extra method-specific type parameters onto the receiver type leaks concerns into the type’s identity and users’ type arguments.

Proposed semantics

  • Declaration

go func (r *R[X]) M[A, B comparable](arg A) B { ... }

where R may or may not be generic. A, B are scoped to the method body/signature.

  • Instantiation / calls

Behave like generic functions. Type arguments may be provided explicitly or inferred:

go q.Include[int](func(*User) *int { ... }) // explicit q.Include(func(*User) *int { ... }) // inferred

  • Receiver visibility Method type parameters may reference receiver parameters (X) and vice versa in constraints where well-formed.

  • Method sets A generic method contributes an infinite family of monomorphized methods. Method selection remains syntactic; there is still one named method Include, whose instantiation is resolved at call sites (like generic functions). No changes to the rules for addressability or method promotion beyond acknowledging generic methods exist.

  • Interfaces Interfaces can declare generic methods:

go type Includeable[T any] interface { Include[F any](func(*T) *F) Includeable[T] }

Implemented by any type providing a matching generic method. This enables ergonomic dependency inversion for fluent builders.

  • Method values/expressions v := q.Include[int] yields a function value with remaining parameters, consistent with generic function behavior.

  • Reflection reflect.Method would report the (possibly generic) signature. Any reflection extensions could follow the precedent set by generic functions (e.g., reporting type parameter lists).

Backward compatibility

No breaking change. Existing code compiles unchanged. Generic methods are purely additive.

Alternatives considered

  • Free functions: work but hurt discoverability, chaining, and OO-style organization.

  • Move type parameters to receiver type: leaks method-local polymorphism into the type’s API and forces needless type arguments everywhere.

  • Use any + assertions: sacrifices type safety and tooling.

Prior art

  • Java, C#, Kotlin allow method-level generics separate from class/type parameters.

  • Rust supports generic methods on impls with fn method(...) where U is method-local.

  • Swift supports generic methods independent of generic types.

Implementation notes

  • Parser: allow optional type parameter list after receiver in method decls.

  • Type checker: treat generic methods analogously to generic functions, with an additional receiver parameter in scope; unify inference across receiver’s type parameters and method-local parameters.

  • Compiler/backend: monomorphization strategy as for generic functions; receiver doesn’t present new challenges beyond passing an extra parameter.

Open questions

  1. Should interface method sets with generic methods impose any additional restrictions (e.g., variance)? (Go has no variance; status quo likely ok.)

  2. Do we need any new reflection APIs, or can we mirror generic function treatment?

  3. Are there corner cases around embedding and promotion when multiple promoted methods share the same name but different method-level type parameter lists?

Conclusion

Allowing type parameters on methods unlocks natural, type-safe fluent APIs and reduces leakage of method-local polymorphism into receiver type parameters. It aligns Go with common practice in other languages while preserving Go’s simplicity: generic methods behave just like generic functions, attached to a receiver.