Go 1.27's generic methods can't satisfy interfaces, on purpose
Go 1.27 gives methods their own type parameters, but bars them from satisfying an interface. Not a missing feature, just what a method table can hold.
Comments
Loading comments
Since generics landed in Go 1.18, a function could have type parameters and a method couldn't. Go 1.27 (opens in new tab), released on August 19, closes that gap: a method can now declare its own type parameters, the way a function always could. The example the release notes lead with is math/rand/v2. Until now the package only had a top-level N[Int intType](n Int) Int, which draws from the default source. If you had your own *Rand, you didn't get a generic N on it, because a method couldn't have type parameters. In 1.27 it does: func (r *Rand) N[Int intType](n Int) Int.
That part is overdue and nice. The part I keep rereading is one sentence at the end of the same paragraph in the release notes (opens in new tab):
Note that methods of interfaces may not declare type parameters nor can interface methods be implemented by generic methods.
So Go added generic methods and, in the same breath, ruled them out of the one place you might most want them. My first reaction was that it sounded like a half-finished feature. After reading the Go team's explanation, I think it's the opposite. The restriction is the honest part of the design.
What you actually get
The release notes are pretty modest about the pitch. Generic methods allow "adding generic functions within the namespace of a particular data type where before one had to declare such functions with a scope of the entire package."
That's it. It's about where a name lives. Mark Freeman's post on the Go blog (opens in new tab) a week later shows the case people have been asking for since 1.18:
list.go
// Go 1.18 through 1.26: a package-level functionfuncMapList[E, R any](l List[E], f func(E) R) List[R] { /* ... */ }// Go 1.27: a method, so calls can chainfunc (List[E]) Map[R any](f func(E) R) List[R] { /* ... */ }
Chaining .Map(...).Map(...) reads better than nesting MapList(MapList(...)). Real improvement. But nothing here changes what a type can promise to code that doesn't know its concrete type, and that's the job interfaces do.
Why a generic method can't fill an interface slot
An interface value has to answer one question at runtime: for this concrete type, which function runs when I call M? Russ Cox's 2009 write-up on Go interfaces (opens in new tab) describes the answer as an itable, a table of function pointers, one per interface method. The runtime computes it by matching the interface's method list against the concrete type's (both sorted, walked together), and then caches it. It doesn't precompute every table up front because, in his words, "there are too many (interface type, concrete type) pairs, and most won't be needed."
The post is old, so I wouldn't lean on its implementation details, but the core idea still holds: one interface method, one function pointer.
Now try to put a generic method in that slot. (*Rand) N[Int intType] isn't one function. It's a family: N[int], N[int64], N[uint8], and so on. There's no single pointer to put in the table. The Go blog puts it more formally: "Interface implementation is a property of a (possibly instantiated) type, not of any particular method." If T has a generic method M, it has T.M, not T.M[int], so it doesn't satisfy an interface asking for a plain M() even when the signatures look identical after instantiation.
The obvious next question is: fine, then let the interface method be generic too. The blog's answer is the part I found most convincing. Imagine package p has an interface I with a generic M[P any](), and a function F(i I) that calls i.M[int](). Package main has a type T and calls p.F(T{}). When main is compiled, it "won't know how p.F uses T{}", including which instantiations of T.M it will want. To be safe, "the compiler would need to instantiate T.M with every possible type argument." That's not a real option.
Go compiles all of this ahead of time. There's no JIT that can stamp out M[float64] the first time a caller asks for it. The blog does mention a way around it: box the method's arguments as values of their constraint interfaces, so every instantiation shares the same code. The catch is "indirect call overhead", and it would hit direct calls to the method too, not just the interface ones. So the Go team picked the simpler rule: "we can't support generic interface methods." I don't read that as a 1.28 TODO.
Rust hit the same wall
Rust has the same problem with trait objects, and it has a name for it. The Rust reference (opens in new tab) calls it dyn compatibility ("formerly known as object safety"). A pointer to a dyn Trait carries a pointer to the value and a vtable that holds, per the trait object docs (opens in new tab), "a pointer to T's implementation (i.e. a function pointer)" for each method. Same shape as Go's itable. And the rule is the same too: a method you can call through dyn must "not have any type parameters (although lifetime parameters are allowed)."
Go isn't installed on the machine I wrote this on, so I couldn't run any Go 1.27 code. The Go snippets above are copied from the release notes and the blog, not things I ran. What I could do was check the Rust side with rustc 1.90.0:
It doesn't compile. Trimmed a little, the error is:
rustc fail.rs
error[E0038]: the trait `Greeter` is not dyn compatible
--> fail.rs:18:20
|
18 | let g: Box<dyn Greeter> = Box::new(Plain);
| ^^^^^^^ `Greeter` is not dyn compatible
|
note: for a trait to be dyn compatible it needs to allow building a vtable
--> fail.rs:3:8
|
3 | fn repeat<T: std::fmt::Display>(&self, x: T) -> String;
| ^^^^^^ ...because method `repeat` has generic type parameters
= help: consider moving `repeat` to another trait
That note says in six words what the Go blog takes a page to explain. I like that Rust just says it.
Two ways out
Rust gives you two fixes, and I ran both.
1
Opt the method out of the vtable
add `where Self: Sized` to `repeat`. The trait is dyn compatible again, because the compiler no longer has to fit `repeat` into the table. It compiled cleanly and printed `hello, world` and then `77` from `Plain.repeat(7)`, called on the concrete type.
2
Check what you gave up
in a copy of that file, calling `g.repeat(7)` on the `Box<dyn Greeter>` fails with "the `repeat` method cannot be invoked on a trait object". So the method still exists, just not through `dyn`.
3
Split the trait
move `repeat` into its own `Repeater` trait and leave `Greeter` with only `greet`. This also compiled cleanly with the same output, and you can hold a `Box<dyn Greeter>` while still calling `repeat` on a concrete `Plain`.
Go has no where Self: Sized escape hatch. A generic method just isn't part of any interface's method set. But the second fix carries over directly. If a type needs a generic operation and also needs to satisfy an interface, those are two different jobs. Put the interface methods in the interface and let the generic method sit on the type.
Namespacing, not abstraction
It's easy to read "Go has generic methods now" and assume interfaces got more powerful too. They didn't, and the release notes say so in one sentence. An interface's method set has to be something you can turn into a fixed table of function pointers. A generic method is a recipe for an open-ended number of functions, and that doesn't fit in a fixed table.
For me this mostly confirms advice Go people already gave: keep interfaces small and concrete. io.Reader is one method, Read(p []byte) (n int, err error), no type parameters, and it gets used everywhere. Generic methods don't change that. What they give you is a better home for the generic helper that was never really an interface method in the first place.
Comments