Generic Methods
The Go Blog
Generic Methods
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:
// 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:
// 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 reasoned that because generic interface methods are difficult to implement efficiently (discussed later), 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):
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:
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]
:
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):
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:
// 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”:
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:
// 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:
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:
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:
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
:
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 on I.M
:
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:
type I interface {
M()
}
type T struct{}
func (T) M() { /* ... */ }
One can call methods on interface values:
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:
// -- 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:
// -- 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!
Previous article: Go 1.27 is released
Blog Index
How it works
Once you click Generate, Ollama reads this article and crafts 5 comprehension questions. Your answers are graded against the article content — general knowledge won't be enough. Score 70+ to count toward your certificate.
Questions are cached — you'll always get the same 5 for this article.