
Cory LaNou
Generic Methods Arrive in Go 1.27
Overview
When generics landed in Go (golang) 1.18, they came with a rule that has annoyed people ever since: functions could declare type parameters, but methods could not. Go 1.27 removes that rule. A method can now declare its own type parameters, which makes patterns like chainable transformations possible for the first time. This was one of the most requested features in the language, and one of the most debated. In this article, we'll look at the problem, the fix, and the restrictions that come with it.
Target Audience
This article is aimed at developers that are comfortable with Go generics basics.
In this article, we'll cover the following topics:
- Why methods couldn't have their own type parameters before Go 1.27
- Declaring a generic method and how type inference applies
- Building a chainable pipeline, which was impossible before Go 1.27
- The restrictions on interfaces and reflection
- Finally, we'll end with an example that puts it all together
The Problem
Say we have a generic Stack[T] and we want a Map operation that transforms every element into a new type U. The receiver already binds T, but U is new. Before Go 1.27, a method had no way to introduce it, so Map was forced to live at the package level:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
// Map can't be a method before Go 1.27, because U is a new type
// parameter. It has to live at the package level.
func MapStack[T, U any](s *Stack[T], f func(T) U) *Stack[U] {
out := &Stack[U]{}
for _, v := range s.items {
out.Push(f(v))
}
return out
}
func main() {
s := &Stack[int]{}
s.Push(1)
s.Push(2)
s.Push(3)
labels := MapStack(s, strconv.Itoa)
fmt.Printf("%q\n", labels.items)
}
$ go run .
["1" "2" "3"]
--------------------------------------------------------------------------------
Go Version: go1.27rc2
This works, but it reads backwards. MapStack(s, f) instead of s.Map(f). The function isn't discoverable from the type, your editor won't suggest it when you type s., and every generic container library in the ecosystem grew a pile of these free-floating helper functions.
The Fix
In Go 1.27, a method declaration may declare its own type parameters. Here's the same Map, where it always belonged:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
// In Go 1.27, a method can declare its own type parameters.
func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U] {
out := &Stack[U]{}
for _, v := range s.items {
out.Push(f(v))
}
return out
}
func main() {
s := &Stack[int]{}
s.Push(1)
s.Push(2)
s.Push(3)
// U is inferred as string from strconv.Itoa.
labels := s.Map(strconv.Itoa)
fmt.Printf("%q\n", labels.items)
// Or instantiate it explicitly.
halves := s.Map[float64](func(i int) float64 { return float64(i) / 2 })
fmt.Println(halves.items)
}
$ go run .
["1" "2" "3"]
[0.5 1 1.5]
--------------------------------------------------------------------------------
Go Version: go1.27rc2
Notice that we never told Map that U was string? Type inference works the same as it does for generic functions, so s.Map(strconv.Itoa) figures it out on its own. And when you want to be explicit, you can instantiate the method directly with s.Map[float64](...).
Chaining
The package-level workaround had one loss that really stung: you couldn't chain transformations. MapStack(FilterStack(s, keep), transform) reads inside out. Now that these operations are methods, pipelines read top to bottom:
type Seq[T any] struct {
items []T
}
func From[T any](items ...T) *Seq[T] {
return &Seq[T]{items: items}
}
// Keep only doesn't need its own type parameter. This was
// already possible with earlier versions of Go.
func (s *Seq[T]) Keep(f func(T) bool) *Seq[T] {
out := &Seq[T]{}
for _, v := range s.items {
if f(v) {
out.items = append(out.items, v)
}
}
return out
}
// Map and Reduce introduce U, so before Go 1.27 they could
// not be methods.
func (s *Seq[T]) Map[U any](f func(T) U) *Seq[U] {
out := &Seq[U]{}
for _, v := range s.items {
out.items = append(out.items, f(v))
}
return out
}
func (s *Seq[T]) Reduce[U any](initial U, f func(U, T) U) U {
acc := initial
for _, v := range s.items {
acc = f(acc, v)
}
return acc
}
type Order struct {
Item string
Total float64
}
func main() {
orders := From(
Order{Item: "gopher plush", Total: 24.99},
Order{Item: "sticker pack", Total: 4.99},
Order{Item: "go course", Total: 299.00},
Order{Item: "coffee mug", Total: 14.99},
)
receipt := orders.
Keep(func(o Order) bool { return o.Total >= 10 }).
Map(func(o Order) string { return o.Item }).
Reduce("receipt:", func(acc, item string) string {
return acc + " " + item
})
fmt.Println(receipt)
revenue := orders.
Map(func(o Order) float64 { return o.Total }).
Reduce(0.0, func(sum, t float64) float64 { return sum + t })
fmt.Printf("revenue: %.2f\n", revenue)
}
$ go run .
receipt: gopher plush go course coffee mug
revenue: 343.97
--------------------------------------------------------------------------------
Go Version: go1.27rc2
Notice the mix here. Keep only works with T, so it was always allowed as a method, while Map and Reduce introduce U, which is what made them impossible before Go 1.27. Now the whole pipeline flows: keep the orders worth at least ten dollars, map them to their names, and reduce down to a receipt line.
I'm not telling you to go rewrite your Go code into functional pipelines. Plain loops are still great, and often clearer. The point is that generic operations on a generic type can finally live on that type, where your editor and your readers expect to find them.
The Restrictions
There are two rules you'll want to know about before you design an API around this.
First, interfaces cannot declare generic methods. This won't compile:
type Converter interface {
Convert[U any](f func(int) U) []U
}
$ go run .
# restrict
./main.go:5:9: interface method must have no type parameters
./main.go:5:29: undefined: U
--------------------------------------------------------------------------------
Go Version: go1.27rc2
The compiler tells you directly. The same rule means a generic method can't be used to implement an interface method. Interface satisfaction in Go is still about concrete method signatures.
Second, generic methods are invisible to reflection. If you call reflect.TypeOf on our Stack and count the methods, you'll see Push but not Map. There is no way to know ahead of time which instantiations of Map a program will need, so the runtime doesn't try. If your code discovers methods via reflection (encoders, RPC frameworks, dependency injectors), generic methods won't show up there.
Neither restriction is a bug. Both fall out of the same design question that kept this feature out of Go 1.18 in the first place. The Go team chose the pragmatic subset that works, and for API design, it's plenty.
Summary
Generic methods close the biggest gap left by Go 1.18. Operations that introduce new type parameters can now live as methods on the type they operate on. Type inference keeps call sites clean. Just remember the two limits: no generic methods in interfaces, and no reflection visibility. If you maintain a generics-heavy package, this is your excuse to delete some awkward package-level functions in your next release.
Want More?
If you've enjoyed reading this article, you may find these articles interesting as well:


