blob: ed59d19ce60e947a656c95f733d745eb8c2c77ed [file] [view] [edit]
---
title: Generic Methods
date: 2026-08-26
by:
- Mark Freeman
summary: Go 1.27 adds generic methods—a highly desired language feature.
template: true
---
Generics was a profound change for Go—it fundamentally expanded the kinds of
programs that gophers could write by adding type parameters to the language.
One could now express generic types and functions in Go. These constructs
decreased the need for specialized data types and functions, reducing program
verbosity and improving the ergonomics of the language in those cases.
To illustrate, different kinds of linked lists could now be condensed into a
single type definition:
```go
// before Go 1.18
type ListOfInts struct {
elem int
next *ListOfInts
}
type ListOfStrings struct {
elem string
next *ListOfStrings
}
// ...
// after Go 1.18
type List[E any] struct {
elem E
next *List[E]
}
```
Likewise, sorting different kinds of ordered data could be expressed as a single
function:
```go
// before Go 1.18
func SortInts(s []int) { /* ... */ }
func SortStrings(s []string) { /* ... */ }
// ...
// after Go 1.18
func Sort[E cmp.Ordered](s []E) { /* ... */ }
```
However, no such capability came to methods.
The generics [proposal](/issue/43651) reasoned that because generic interface
methods are difficult to implement efficiently (discussed
[later](#the-trouble-with-generic-interface-methods)), it didn't make sense to
add type parameters to non-interface (or "concrete") methods either. In that
view, methods are primarily the means of implementing an interface.
Go 1.27 takes a different view and consequently adds generic methods to Go. In
this post, we'll explain this change of view and demonstrate some uses of this
new feature.
# Methods for organization
Methods enable organizing functionality around types. To illustrate, let's
reconsider the linked list from above (adding some conveniences):
```go
type List[E any] struct {
elem E
next *List[E]
}
func NewList[E any](elems ...E) List[E] { /* ... */ }
func (List[E]) String() string { /* ... */ }
```
Suppose that one wanted to map this structure to hold a value of some other
type, like a `string`. A method like `ToString` would do:
```go
func (List[E]) ToString(f func(E) string) List[string] { /* ... */ }
```
By providing a “transform” function `f`, `ToString` can be customized with
“off-the-shelf” routines.
For example, `strconv.Itoa` might work for a `List[int]`:
```go
func main() {
fmt.Println(NewList(1, 2, 3).ToString(strconv.Itoa)) // [1 2 3]
}
```
For a `List[[]byte]`, more options come to mind (depending on the use case):
```go
func main() {
l := NewList([]byte("Hallo Welt"), []byte("Helló világ"))
fmt.Println(l.ToString(hex.EncodeToString)) // [48616c6c6f2057656c74 48656c6cc3b32076696cc3a167]
fmt.Println(l.ToString(base64.StdEncoding.EncodeToString)) // [SGFsbG8gV2VsdA== SGVsbMOzIHZpbMOhZw==]
}
```
Note that parameterizing `List` allowed generalization of the *source* type, but
not the *destination* type, since that depends on the transformation being
applied. This might be reasonable with just a single destination type, but what
if there were many?
In Go 1.18, a generic function could be used for this:
```go
// after Go 1.18
func MapList[E, R any](l List[E], f func(E) R) List[R] { /* ... */ }
```
But using a function has the disadvantage of moving `MapList` into the package
scope—if many data types support mapping operations, things could get crowded.
Furthermore, chained calls must be written “inside out”:
```go
func main() {
fmt.Println(MapList(MapList(NewList(0, 2, 4), add(2)), divideBy(2))) // [1 2 3]
}
```
Because Go 1.18 didn’t support generic methods, we needed a generic function as
a workaround. This is now remedied in Go 1.27:
```go
// after Go 1.27
func (List[E]) Map[R any](f func(E) R) List[R] { /* ... */ }
```
This is compact, expressive, *and* locally scoped. Additionally, chained calls
are more readable, as they can be written more naturally from left to right:
```go
func main() {
fmt.Println(NewList(0, 2, 4).Map(add(2)).Map(divideBy(2))) // [1 2 3]
}
```
Prefer the “inside out” form? A method expression can convert any method,
including a generic one, into its function equivalent. Thus, a function call
structure can be recovered if desired:
```go
func main() {
f := List[int].Map[int]
fmt.Println(f(f(NewList(0, 2, 4), add(2)), divideBy(2))) // [1 2 3]
}
```
Generic methods, like other generics in Go, must be instantiated (either
explicitly or implicitly) before they are used (called or converted to a
function).
# Decoupling things
If one views methods *also* as an organizational tool, then Go 1.18's reasoning
appears overly restrictive. While a generic concrete method can't help implement
an interface (without generic interface methods), it can still be useful for
code organization. In other words, these concerns can be decoupled.
To elaborate, let's consider a simple interface:
```go
type I interface {
M()
}
type T struct{} // a struct, not an interface
func (T) M[P any]() { /* ... */ } // a generic *concrete* method
```
Here, `T.M` is parameterized by `P`. Any instantiation of `T.M` would result in
a method signature identical to that of `I.M`:
```go
func main() {
T{}.M[int]()
}
```
Both `T.M[int]` and `I.M` have the signature `func M()`. However, this doesn’t
mean `T` implements `I`. Interface implementation is a property of a (possibly
instantiated) type, not of any particular method. Importantly, `T` does not
declare `T.M[int]`, but rather its generic (and thus uninstantiated) counterpart
`T.M`.
For `T.M` to participate in interface implementation, there would need to exist
a suitable generic method `I.M`:
```go
type I interface {
M[P any]() // a generic *interface* method
}
type T struct{}
func (T) M[P any]() { /* ... */ }
```
Since the Go 1.27 syntax doesn’t allow interface methods to declare type
parameters, this code is impossible to write.
But *why* can’t Go have generic interface methods? To understand, a bit of a
detour is needed.
# The trouble with generic interface methods
An interface value is a box that can hold some other value—the boxed value can
be of any type, as long as that type implements the interface. This means that
the declared methods on the type are a superset of those declared on the
interface.
To illustrate, `T` implements `I` below:
```go
type I interface {
M()
}
type T struct{}
func (T) M() { /* ... */ }
```
One can call methods on interface values:
```go
func main() {
F(T{})
}
func F(i I) {
i.M() // "backed" by T.M
}
```
Above, it’s easy to observe that the call to `i.M` will be routed to `T.M`. But
in more complex programs, the relation between an interface value method call
and its *possible* targets is significantly less obvious. This relation
typically spans across packages, making it difficult or impossible to deduce in
the general case.
To illustrate, let’s introduce a package boundary:
```go
// -- package main --
import "p"
func main() {
p.F(T{})
}
type T struct{}
func (T) M() { /* ... */ }
// -- package p --
func F(i I) {
i.M()
}
type I interface {
M()
}
```
Because the two packages are compiled separately, the `main` package won’t know
how `p.F` uses `T{}`. To ensure the existence of any method that `p.F` *might*
call, the compiler generates code for *all* non-generic methods of a type at its
declaration (or instantiation). That way, regardless of what `p.F` does with
`T{}`, the necessary code will exist at runtime.
Now, suppose that `I.M` were generic:
```go
// -- package main --
import "p"
func main() {
p.F(T{})
}
type T struct{}
func (T) M[P any]() { /* ... */ }
// -- package p --
func F(i I) {
i.M[int]()
}
type I interface {
M[P any]()
}
```
Again, the `main` package won’t know how `p.F` uses `T{}`—including how it might
instantiate `T.M`. To handle *any* usage of `T{}` inside `p.F`, the compiler
would need to instantiate `T.M` with *every possible type argument*.
This is impractical with Go’s approach to instantiation, wherein the compiler
generates specific code for each method based on the type arguments. If method
arguments were “boxed” (i.e. passed as values of their constraint interfaces),
each instantiation would share the same code. This would avoid having too many
instantiations at the cost of indirect call overhead—even for direct calls to
instantiated methods.
# Conclusion
Go 1.27 introduces type parameters on concrete methods—this feature has been
highly desired by the Go community, as it permits more ergonomic and readable
code. Although we can’t support generic interface methods, we decided that
allowing type parameters for concrete methods was still worthwhile.
We hope you enjoy using Go’s generic methods and find helpful ways to use them
in your projects!