10 min left
2083 words
10 minutes

Generic Methods Coming to Go

Generic Methods Coming to Go#

“We do not anticipate that Go will ever add generic methods.”

— Go FAQ, for roughly a decade.

That line sat in the FAQ like a monument. It wasn’t a “not yet.” It was a “never.” And for years, every time someone opened an issue asking for type parameters on methods, that quote was the mic drop that ended the conversation.

Then, in January 2026, Robert Griesemer opened proposal #77273: “Generic methods for concrete types.” The label was added: Proposal-Accepted.

Before you get too excited: this is an accepted proposal, not a shipped feature. You cannot use this in any Go release today. That said, the path forward is clear, and understanding what’s coming, and what isn’t, is worth your time now.

The Status Quo: The Anti-Pattern We All Live With#

Since Go 1.18 gave us generics, there’s been a frustrating gap. You can write generic functions and generic types, but you can’t combine them into generic methods. The method set of a generic type can only contain non-generic methods.

This means many post-1.18 Go codebases have functions like these scattered around:

type Cache[K comparable, V any] struct {
items map[K]V
}
// This is fine: non-generic method on a generic type.
func (c *Cache[K, V]) Get(key K) (V, bool) {
v, ok := c.items[key]
return v, ok
}
// But you can't do this:
// func (c *Cache[K, V]) Transform[T any](fn func(V) T) []T { ... }
// So you end up with this:
func Transform[K comparable, V any, T any](c *Cache[K, V], fn func(V) T) []T {
result := make([]T, 0, len(c.items))
for _, v := range c.items {
result = append(result, fn(v))
}
return result
}

That last function takes the receiver as its first argument. It’s not a method. It doesn’t show up on autocomplete, can’t be chained, and breaks the left-to-right reading flow that methods provide. cache.Transform(...) reads naturally; Transform(cache, ...) reads like inside-out code. This isn’t just aesthetics: it makes APIs harder to discover, document, and compose.

Why This Became Possible#

The reason generic methods were blocked for so long wasn’t technical laziness. It was a genuine design problem.

The original Type Parameters proposal discussed generic methods and rejected them. The reasoning went like this: if methods can have type parameters, then interface methods should too. And generic interface methods are genuinely hard. They break type erasure, create dynamic dispatch problems, and don’t play well with reflection. So the whole idea was shelved.

What changed? Griesemer’s insight, captured in the proposal, is disarmingly simple:

“Concrete methods are a language feature that is useful in itself, irrespective of interfaces.”

Methods do two things: they implement interfaces, and they organize code on a type. The Go team had been conflating these two roles. Once you separate them, the problem dissolves:

// This is what's being added (concrete type, generic method):
func (s *MySlice[E]) Map[T any](fn func(E) T) []T
// This is NOT being added (interface method with type parameters):
type Reader[E any] interface {
Read[E]() // Still impossible
}

The first one is straightforward. The compiler knows the concrete type at compile time, so it can statically resolve the method using the existing type argument mechanism. No dynamic dispatch, no reflection, no runtime complexity.

The second one remains unsolved because Reader[E] doesn’t name a single method. It names a family of methods, and Go’s interface satisfaction model can’t handle that.

This decoupling is the key insight. Generic methods on concrete types were always implementable. They were just held hostage to the harder problem of generic interface methods.

The history matters here. Issue #49085, opened in October 2021 by the community, accumulated over 900 positive reactions. It was the primary pressure point. Issue #50981 followed in February 2022 with simpler motivating examples. Both were effectively deferred. Until now.

The Syntax#

The grammar change is minimal. The method declaration production gains an optional type parameter list:

MethodDecl = "func" Receiver MethodName [ TypeParameters ] Signature [ FunctionBody ]

In practice:

type Stack[T any] struct {
items []T
}
// Regular method on a generic type (valid since Go 1.18):
func (s *Stack[T]) Push(items ...T) {
s.items = append(s.items, items...)
}
// Generic method with its own type parameter: new!
func (s *Stack[T]) Map[U any](fn func(T) U) *Stack[U] {
result := &Stack[U]{}
for _, item := range s.items {
result.Push(fn(item))
}
return result
}
// Calling it:
nums := &Stack[int]{}
nums.Push(1, 2, 3)
strs := nums.Map[string](func(n int) string { return strconv.Itoa(n) })
// Type inference works:
strs = nums.Map(strconv.Itoa)

There’s a subtler grammar change too: TypeArgs moves from Operand to PrimaryExpr. This is what makes expr.Method[T](args) parseable. The type arguments attach to the method call expression, not just to identifiers.

Method expressions and method values work as you’d expect, with one catch:

// Method expression: Stack[int].Map produces a generic function
// with signature [U any](*Stack[int], func(int) U) *Stack[U]
// Method value: s.Map produces [U any] func(func(int) U) *Stack[U]
// But this is INVALID: you must instantiate the type first:
// Stack.Map[U any] // ERROR: Stack is not instantiated

The Boundary: What This Does NOT Do#

After seeing the syntax, the natural next question is: “Can I use this in interfaces?”

No.

Generic interface methods are explicitly out of scope. This is a feature that solves the right problem first. The interface problem is a different, harder one, and leaving it unsolved here doesn’t preclude a future proposal.

The Case Against#

I’d be dishonest if I didn’t engage with the real objections.

“Go said never. Why should we trust them now?”

The FAQ reversal is uncomfortable. When language designers draw a line, reversing it risks eroding trust. Some developers chose Go because of its restraint. Adding features incrementally is one thing; reversing explicit “never” statements is another.

My read: the original resistance was principled but overly conservative. The reasoning was “generic methods require generic interface methods,” and that premise turned out to be false. Changing course when you discover a false premise isn’t flip-flopping. It’s good engineering. The Go team has a track record of shipping minimal, well-thought-out language changes. I’d rather they correct a mistake than defend it out of pride.

“This adds complexity for marginal benefit.”

This is the strongest objection. Go’s complexity budget is real. Every new feature makes the language harder to learn, harder to tool, and harder to reason about. Is the convenience of cache.Transform(...) worth the cognitive cost of another grammar production?

I think so, for two reasons. First, the implementation cost is surprisingly low. The compiler can statically resolve generic methods on concrete types without any runtime changes. This isn’t adding a new dispatch mechanism; it’s removing a parsing restriction. Second, the ergonomic benefit is disproportionate. I keep running into the (receiver, typeParam) pattern in post-1.18 Go codebases. Fixing it at the language level eliminates a class of API awkwardness that I encounter daily.

“This opens the door to generic interface methods, and then we’re Java.”

Slippery slope arguments are easy to make. But the proposal explicitly addresses this: “It also doesn’t preclude the implementation of generic interface methods at some point, should we find an acceptable implementation solution.” This is honest. It doesn’t promise never, and it doesn’t promise soon. It leaves the door open for a future proposal that makes its own case.

I’d argue the opposite slippery slope: if Go doesn’t ship generic methods on concrete types, the pressure for full generic interface methods only grows. Shipping this targeted feature might actually reduce demand for the more complex one by solving the practical pain point.

Practical Examples#

Let me show you what this actually looks like in code you might write.

Type-Safe Builders#

type Query[T any] struct {
filters []func(T) bool
limit int
}
func NewQuery[T any]() *Query[T] {
return &Query[T]{limit: -1}
}
func (q *Query[T]) Where(pred func(T) bool) *Query[T] {
q.filters = append(q.filters, pred)
return q
}
func (q *Query[T]) Limit(n int) *Query[T] {
q.limit = n
return q
}
func (q *Query[T]) Run(items []T) []T {
// Applies all filters and returns matching items (implementation omitted)
}
// Usage: notice how the type parameter flows through the chain:
type User struct {
Name string
Age int
}
users := []User{{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}}
activeAdults := NewQuery[User]().
Where(func(u User) bool { return u.Age >= 18 }).
Where(func(u User) bool { return u.Name != "" }).
Limit(10).
Run(users)

Today, you’d need to make Where a package-level function that takes the query as its first argument. The chaining breaks. The type parameter doesn’t flow naturally. With generic methods, the builder pattern becomes first-class.

GroupBy: A Method-Level Type Parameter#

Here’s a case where the method genuinely needs its own type parameter, unrelated to the receiver’s:

type Table[R any] struct {
rows []R
}
func (t *Table[R]) GroupBy[K comparable](keyFn func(R) K) map[K][]R {
groups := make(map[K][]R)
for _, row := range t.rows {
k := keyFn(row)
groups[k] = append(groups[k], row)
}
return groups
}
// Usage:
orders := &Table[Order]{rows: allOrders}
byCustomer := orders.GroupBy[string](func(o Order) string { return o.CustomerID })

The type parameter K belongs to GroupBy, not to Table. This is the kind of thing you simply cannot express as a method today. With generic methods, it becomes natural.

The List[E].Format[F] Pattern#

This is the motivating example from the proposal itself: a method that introduces its own type parameter independent of the type’s parameter:

type List[E any] struct {
elements []E
}
func (l *List[E]) Format[F any](formatter func(E) F) []F {
result := make([]F, len(l.elements))
for i, e := range l.elements {
result[i] = formatter(e)
}
return result
}
// E is string, F is int: two independent type parameters
names := &List[string]{elements: []string{"hello", "world"}}
lengths := names.Format[int](func(s string) int { return len(s) })
// lengths == []int{5, 5}

This is the case that’s truly impossible to express cleanly today. The method needs a type parameter (F) that’s unrelated to the type’s parameter (E). A package-level function can do it, but then you lose the method call syntax entirely.

How to Prepare Today#

You can’t use generic methods yet, but you can write code that migrates trivially when they land.

1. Wrap your (receiver, typeParam) functions on types.

If you have:

func Transform[K comparable, V any, T any](c *Cache[K, V], fn func(V) T) []T {
// ...
}

Keep the type as the first argument. When generic methods ship, the migration is mechanical: move the function onto the type, drop the receiver parameter, done. The function body doesn’t change.

2. Stop reaching for interface{} workarounds.

If you’re using empty interfaces to work around the inability to write generic methods, stop. Design your types with proper type parameters now. The code will work today with package-level functions and become cleaner tomorrow with methods.

3. Name your helper functions after the future method.

If you plan to add a Transform method to your Cache type, name the package-level function Transform, not TransformCache or ApplyToCache. Mechanical migration depends on name alignment.

When Will It Land?#

Here’s where things stand:

  • Parser: Already handles type parameters on methods (parses them, currently rejects with an error). The change to accept them is trivial.
  • Type checker: Needs modifications to remove the current restriction and handle method expressions/values with type parameters. In progress.
  • Compiler backend: Generic method calls on concrete types can be statically resolved and rewritten as generic function calls. This is well-understood: no new dispatch mechanism needed.
  • Export/import format: This is the hardest part. The serialized format for compiled packages needs to represent generic methods, and changing it affects tooling across the ecosystem (gopls, vulncheck, build cache compatibility). This is what determines the timeline.

The x/tools repository already has a tracking issue (#77549) for generic method support across the toolchain. The Go team typically allows one or two release cycles for tooling to catch up after a language change.

Go 1.27 or 1.28 feels realistic. This is a well-scoped change with a clear implementation path, not a years-long research project like the original generics design.

My Take#

I’m genuinely excited about this, and not just because I get to delete a category of awkward functions from my codebases.

What I appreciate about this proposal is the discipline. The Go team could have swung for the fences and tried to solve generic interface methods too. Instead, they identified the part that’s well-understood, implementable, and high-value, and they scoped just that. That’s the kind of restraint that made Go worth using in the first place.

What I’d warn against: don’t use generic methods as a hammer. If a package-level generic function reads clearly, keep it as a function. Methods aren’t inherently better. They’re better when they improve API discoverability and composability. Use the feature where it earns its complexity budget, not everywhere it’s syntactically legal.

Go’s relationship with generics has been cautious, sometimes frustratingly so. But this proposal feels like the right next step: small, principled, and immediately useful. The FAQ was wrong. That’s okay. Recognizing a mistake and correcting it is better than defending it indefinitely.

Generic Methods Coming to Go
https://corentings.dev/blog/generic-methods-coming-to-go/
Author
Corentin Giaufer Saubert
Published at
2026-04-27
License
CC BY-NC-SA 4.0
Share this post