Generic Methods in Go 1.27: What Changed, What Didn't
For as long as Go has had generics, a method could only use the type parameter its receiver declared, so producing anything else meant a package-level function taking the receiver as its first argument. Go 1.27 changes that, but only partway…

For as long as Go has had generics, a method could only use the type parameter its receiver declared, so producing anything else meant a package-level function taking the receiver as its first argument. Go 1.27 changes that, but only partway…
Generic Methods in Go 1.27: What Changed, What Didn't
I have written more package-level functions than I wanted to, for one reason: a method on a generic type could only work with the type parameter its receiver already carried. If Store[E] needed to hand back something other than E, the receiver's type parameter had nowhere to put the new one, so the operation had to leave the method set entirely and become a free function that took the store as its first argument. Go 1.27 lifts that restriction for methods on concrete types, and the same release widens generic function type inference into four assignment contexts that used to refuse it outright.
It does not do either of those things everywhere. Interface methods still cannot declare type parameters, and the builtin append still will not infer an uninstantiated generic function passed to it, on either toolchain. I built a small in-memory store, a chained query builder, and a set of inference fixtures against go1.27rc1 to find the exact boundary rather than take the release notes at their word. Source and tests are in 01-generic-methods in the companion repo.
Written and benchmarked against
go1.27rc1, cut fromrelease-branch.go1.27on 18 June 2026. The release notes carried a draft warning at the time of writing. Final release is 25 August 2026.
Problem
Before Go 1.27, a generic type's methods were stuck with whatever type parameter the receiver declared. Say Store[E] needs to project its elements into some other type, group them by an arbitrary key type, or fold them into an accumulator of a third type. None of those output types is E, and a method has no type parameter list of its own to hold them. The only way to write the operation at all was a package-level function that took the store as an ordinary argument:
// ProjectStore is the pre-1.27 equivalent of (*Store[E]).Project.
func ProjectStore[E, R any](s *Store[E], f func(E) R) []R {
out := make([]R, len(s.items))
for i, e := range s.items {
out[i] = f(e)
}
return out
}That compiles from Go 1.18 onwards and works, but it costs something at the call site. It cannot be chained, so a pipeline of operations reads inside out: the innermost call happens first, and everything wraps outward from there. A query builder makes the cost concrete. Filtering a store and then collecting the survivors into a different type used to have to look like this:
CollectQuery(store.Query().Where(published), byID) // pre-1.27
store.Query().Where(published).Collect(byID) // Go 1.27The left-hand form is CollectQuery wrapped around a .Query().Where(...) chain, because the terminal projection step could not be a method: it needed a type parameter the receiver's E did not supply. The right-hand form only became legal in Go 1.27, once a method could declare R for itself.
Deep Dive
Go 1.27 lets a method declare a type parameter list in addition to whatever its receiver already has. Store[E] in store.go declares three: Project[R any], GroupBy[K comparable], and Fold[A any]. Query[E] declares a fourth, Collect[R any], which is what makes the chained form above legal. Each generic method sits beside the package-level function that was the only way to express the same operation before: ProjectStore, GroupStoreBy, FoldStore, CollectQuery. Both forms are compiled and tested against the same inputs in the companion directory, so the difference is something I could compare directly rather than describe from memory.
The restriction is gated on the module's language version, not just the toolchain. Compiling one of the generic methods with go1.27rc1 against a go.mod declaring go 1.26 fails with:
generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)So having the right compiler installed is not enough. The module has to opt in.
Function type inference got wider, in four specific places. Go 1.21 taught the compiler to infer an uninstantiated generic function's type arguments when it was assigned to a variable of function type, returned, assigned to a field, or passed as an ordinary call argument. Go 1.27 extends that into composite literal elements and channel sends. I compiled each context in isolation under both toolchains, assigning NotBlank[S ~string] to a named function type FieldRule without instantiating it:
| Context | go1.26.5 | go1.27rc1 |
|---|---|---|
var r FieldRule = NotBlank | infers | infers |
return NotBlank | infers | infers |
p.Primary = NotBlank | infers | infers |
takesRule(NotBlank) (call argument) | infers | infers |
Policy{Primary: NotBlank} (struct literal) | fails | infers |
[]FieldRule{NotBlank} (slice literal) | fails | infers |
map[string]FieldRule{"a": NotBlank} | fails | infers |
ch <- NotBlank (channel send) | fails | infers |
append(b, NotBlank) | fails | fails |
Four contexts started inferring in go1.27rc1: struct composite literals, slice literals, map literal values, and channel sends. The other four already inferred under go1.26.5, and I am listing them so the split is precise: half of what a reader might credit to Go 1.27 was already working two versions ago.
The struct literal case is the one worth a closer look, because it depends on the field selector doing the work a positional argument cannot. Policy{Primary: NotBlank} names the field explicitly, and the compiler reads FormPolicy.Primary's declared type, FieldRule, to know what to instantiate NotBlank against. Take the field selector away and there is no type to infer against, which is exactly why this context needed its own compiler change rather than falling out of the 1.21 work for free.
The standard library carried the same workaround, and Go 1.27 let it drop part of it. math/rand/v2 needed a generic draw and, before 1.27, a method could not declare a type parameter, so the only option was a package-level function:
| Toolchain | Package function | Method |
|---|---|---|
| go1.26.5 | func N[Int intType](n Int) Int at rand.go:323 | absent |
| go1.27rc1 | func N[Int intType](n Int) Int at rand.go:333 | func (r *Rand) N[Int intType](n Int) Int at rand.go:210 |
Both forms exist in go1.27rc1. The function was not removed or deprecated, it stopped being the only option. The difference is testability. The package-level function draws from the global source shared with the rest of the process, so nothing pins its sequence. The method draws from a *Rand the caller constructed and seeded, so a test can assert an exact schedule.
Two things do not compile, at two different compiler stages. An interface method still cannot declare a type parameter, because the compiler would need one method-set entry per possible type argument, which it has no way to enumerate. That one is rejected by the parser, before type checking ever runs:
$ go1.27rc1 build ./testdata/interface_method_typeparams.go
# command-line-arguments
testdata/interface_method_typeparams.go:24:9: interface method must have no type parameters
testdata/interface_method_typeparams.go:24:33: undefined: RThe second line is fallout from the first: once the parser rejects the method declaration, R is never in scope, so the reference to it is reported too. It is not a second, independent problem.
append is different. append(b, NotBlank) parses cleanly and fails in the type checker:
$ go1.27rc1 build ./testdata/appendinference/append.go
# command-line-arguments
testdata/appendinference/append.go:30:22: cannot use generic function NotBlank without instantiationThis is not a regression. append(b, NotBlank) fails identically on go1.26.5, so nothing about it changed in go1.27rc1, it never worked. What makes it worth writing up is that it is specific to the builtin, not to variadic arguments in general. A user-defined variadic function accepts the same uninstantiated NotBlank without complaint:
| Call | go1.27rc1 |
|---|---|
takesRules(NotBlank) where func takesRules(rs ...FieldRule) | infers |
takesRules(NotBlank, NotBlank) | infers |
append(b, NotBlank) | fails |
append(b, NotBlank[string]) | compiles |
A variadic argument is an assignment context by the spec's own definition, so append's variadic slot reads like it ought to infer under the same rule that made ordinary call arguments infer since 1.21. It does not. Explicit instantiation is the workaround, and it costs nothing at runtime.
Code Examples
The generic method and its pre-1.27 equivalent, side by side, from store.go:
// Project applies f to every element in insertion order and returns the
// results. R is the method's own type parameter, inferred at the call site from
// f's return type, so one store can produce slices of any element type.
func (s *Store[E]) Project[R any](f func(E) R) []R {
out := make([]R, len(s.items))
for i, e := range s.items {
out[i] = f(e)
}
return out
}// GroupBy buckets the stored elements by the key f returns. K is constrained to
// comparable because it is used as a map key; E is not, which is why the two
// type parameters have to be declared separately.
func (s *Store[E]) GroupBy[K comparable](f func(E) K) map[K][]E {
groups := make(map[K][]E)
for _, e := range s.items {
k := f(e)
groups[k] = append(groups[k], e)
}
return groups
}The four newly inferring contexts, from inference.go:
// DefaultRules is a slice composite literal whose elements are uninstantiated
// generic functions. Under go1.26.5 each element is rejected with
// "cannot use generic function NotBlank without instantiation".
var DefaultRules = []FieldRule{
NotBlank,
NoControlChars,
ASCIIOnly,
}// StrictPolicy is a struct composite literal with uninstantiated generic
// functions as field values.
var StrictPolicy = FormPolicy{
Name: "strict",
Primary: NotBlank,
Fallback: ASCIIOnly,
}// SendDefaultRules pushes the default rules down a channel. A channel send is
// an assignment context, and it is the fourth one that only started inferring
// in go1.27.
func SendDefaultRules(ch chan<- FieldRule) {
ch <- NotBlank
ch <- NoControlChars
ch <- ASCIIOnly
}append still refusing the same function, and the fix, also from inference.go:
// AppendDefaultRules extends base with the default rules.
//
// The natural spelling is:
//
// return append(base, NotBlank, NoControlChars, ASCIIOnly)
//
// go1.27rc1 rejects it:
//
// cannot use generic function NotBlank without instantiation
//
// append's variadic arguments are an assignment context by the spec's own
// definition, so by the release note wording this ought to infer. It does not.
// See testdata/appendinference/append.go and TestAppendNeedsExplicitInstantiation,
// which compile that source with go/types and assert the error.
//
// Explicit instantiation is the workaround and costs nothing at runtime.
func AppendDefaultRules(base []FieldRule) []FieldRule {
return append(base,
NotBlank[string],
NoControlChars[string],
ASCIIOnly[string],
)
}rand.N as a method, drawing from a seeded, caller-owned source, against the package function drawing from the global one, from randn.go:
func (p *BackoffPolicy) Delay(attempt int) time.Duration {
capped := p.Base << attempt
if capped > p.Max || capped <= 0 {
capped = p.Max
}
// The generic method, new in go1.27rc1.
//
// time.Duration is an int64, so it satisfies the intType constraint and
// Int is inferred as time.Duration. No conversion to int64 and back is
// needed, and the result is already a Duration.
return p.rand.N(capped)
}// DelayViaPackageFunc is the pre-1.27 shape, kept for contrast.
//
// It reaches the same result, but only by drawing from the global source. The
// policy's own seeded *Rand is ignored, so two policies seeded differently
// produce the same sequence here, and a test cannot pin it.
//
// This is the workaround the standard library itself shipped until the method
// became expressible.
func DelayViaPackageFunc(base, max time.Duration, attempt int) time.Duration {
capped := base << attempt
if capped > max || capped <= 0 {
capped = max
}
return rand.N(capped)
}The interface fixture that the parser rejects, in full, from testdata/interface_method_typeparams.go:
type Article struct {
Title string
}
// Repository tries to put a type parameter on an interface method.
type Repository interface {
// Project is the method that cannot be declared.
Project[R any](f func(Article) R) []R
}
func main() {}The append fixture that the type checker rejects, in full, from testdata/appendinference/append.go:
type FieldRule func(string) error
func NotBlank[S ~string](v S) error { return nil }
func main() {
var base []FieldRule
// The natural spelling, rejected by go1.27rc1.
base = append(base, NotBlank)
_ = base
}Both fixtures carry //go:build ignore and are exercised by TestInterfaceMethodCannotDeclareTypeParameters and TestAppendArgumentDoesNotInfer in compileerrors_test.go, which parse and type-check them with go/parser and go/types and assert the exact diagnostic text. If a future release changes either behaviour, those tests fail rather than this post quietly going stale.
Lessons Learned
The two failures teach different things, and I would not have separated them cleanly without reading the fixtures side by side. The interface method case is a structural limit: the compiler cannot build a method set for an interface whose methods carry their own type parameters, because it would need one entry per type argument a caller might supply, and that set is unbounded. Nothing about widening inference or extending literal contexts touches that limit, and I do not expect it to move.
The append case is narrower and, if I am honest, more interesting, because it is inconsistent rather than fundamental. A user-defined variadic function infers an uninstantiated generic function argument without complaint in go1.27rc1. The builtin, taking arguments in the same syntactic position, does not. That tells me the inference extension was implemented against specific syntactic forms (composite literals, channel sends, ordinary and variadic call arguments to user functions) rather than against the general rule the release notes describe. append is a builtin with special-cased type checking, not an ordinary variadic function, and the special casing evidently was not extended alongside everything else. Explicit instantiation costs nothing at runtime, so it is a one-line workaround, but it is a gap I would not have found without compiling the failing case myself rather than trusting the description of what "assignment contexts" covers.
rand.N was the clearest illustration in the whole directory, because it is not a contrived example. The standard library needed the same thing application code needed: a generic draw bound to one instance rather than a shared global. Before 1.27 it had exactly the same workaround available to any of us, a package-level function, and it used it. Go 1.27 let it add the method without removing the function, which is presumably deliberate: existing callers of the package-level rand.N do not break, and new code that cares about seeded reproducibility has a method to reach for instead.
Takeaways
- A method on a concrete type may now declare its own type parameters in Go 1.27, so operations like
Project,GroupBy,Fold, andCollectcan be methods instead of package-level functions taking the receiver as an argument. Interface methods still cannot. - Compiling a generic method against a
go.moddeclaringgo 1.26fails withgeneric method requires go1.27 or later, even under thego1.27rc1toolchain. The module's language version gates the feature, not just the installed compiler. - Function type inference widened in exactly four contexts in
go1.27rc1: struct composite literals, slice literals, map literal values, and channel sends. Var declarations, return statements, field assignment, and ordinary call arguments already inferred before 1.27. Do not credit all eight to this release. append(b, NotBlank)fails identically ongo1.26.5andgo1.27rc1. It is not a regression, it never worked, and the gap is specific to the builtin: a user-defined variadic function accepts the same uninstantiated argument without complaint.math/rand/v2kept its package-levelNfunction and added(*Rand).Nas a method ingo1.27rc1. Reach for the method when a test needs a reproducible sequence from a seeded source; the function still draws from the global one.- The two things that do not compile fail at different compiler stages: an interface method with type parameters is rejected by the parser, before type checking runs.
appendwith an uninstantiated generic function is rejected by the type checker. That is why their error output looks so different.
Full source, tests, and the fixtures that pin both failing cases are in 01-generic-methods in the companion repo.