Generics arrived in Go 1.18, but one conspicuous restriction remained: a method could not declare type parameters of its own. Go 1.27 removes that restriction for concrete methods. This is a small grammar change with a large design effect: generic operations can finally live next to the types they belong to.
Every snippet runs on Go 1.27 via Codapi sandboxes directly in your browser. The "before" examples use a Go 1.26 badge.
The missing piece
Before: type-changing operations were free functions
Suppose a query returns rows of one type and a caller wants to project them into another. Before Go 1.27, the type-changing operation had to be declared at package scope:
package main
import (
"fmt"
"strings"
)
type Query[T any] struct {
Rows []T
}
func Map[T, U any](query Query[T], transform func(T) U) Query[U] {
rows := make([]U, len(query.Rows))
for i, row := range query.Rows {
rows[i] = transform(row)
}
return Query[U]{Rows: rows}
}
func main() {
query := Query[string]{Rows: []string{"alpha", "beta"}}
lengths := Map(query, func(name string) int { return len(strings.TrimSpace(name)) })
fmt.Println(lengths.Rows)
}This works, but Map occupies the package namespace and the data flows inside-out through nested function calls.
After: methods may declare type parameters
Go 1.27 allows a type parameter list after a concrete method name:
package main
import (
"fmt"
"strings"
)
type Query[T any] struct {
Rows []T
}
func (q Query[T]) Map[U any](transform func(T) U) Query[U] {
rows := make([]U, len(q.Rows))
for i, row := range q.Rows {
rows[i] = transform(row)
}
return Query[U]{Rows: rows}
}
func main() {
query := Query[string]{Rows: []string{"alpha", "beta"}}
lengths := query.Map(func(name string) int { return len(strings.TrimSpace(name)) })
fmt.Println(lengths.Rows)
}T belongs to the receiver type. U belongs only to the Map method. The compiler infers U as int from the callback.
The declaration reads like an ordinary generic function with a receiver added:
func (q Query[T]) Map[U any](transform func(T) U) Query[U]Type-changing APIs
Methods can return a new instantiation
A generic method is not limited to preserving its receiver's element type. It can move from Box[T] to Box[U]:
package main
import (
"fmt"
"strconv"
)
type Box[T any] struct {
Value T
}
func (b Box[T]) Map[U any](fn func(T) U) Box[U] {
return Box[U]{Value: fn(b.Value)}
}
func main() {
port := Box[int]{Value: 8080}
label := port.Map(strconv.Itoa)
fmt.Printf("%T %+v\n", label, label)
}Errors can travel through the same transformation without giving up the result type:
package main
import (
"fmt"
"strconv"
)
type Result[T any] struct {
Value T
Err error
}
func (r Result[T]) Then[U any](fn func(T) (U, error)) Result[U] {
if r.Err != nil {
return Result[U]{Err: r.Err}
}
value, err := fn(r.Value)
return Result[U]{Value: value, Err: err}
}
func main() {
input := Result[string]{Value: "27"}
parsed := input.Then(strconv.Atoi)
fmt.Printf("value=%d err=%v\n", parsed.Value, parsed.Err)
}The method call makes the pipeline read left to right. It does not turn Go into a fluent-programming language automatically: every returned type still needs to declare the next method in the chain.
Inference and explicit arguments
Calls usually infer the new type
Method type arguments follow the same inference rules as generic functions. A callback or ordinary argument often provides everything the compiler needs:
package main
import "fmt"
type Collector struct{}
func (Collector) Pair[A, B any](left A, right B) struct {
Left A
Right B
} {
return struct {
Left A
Right B
}{left, right}
}
func main() {
pair := (Collector{}).Pair("attempts", 3)
fmt.Printf("%s=%d\n", pair.Left, pair.Right)
}Type arguments can still be explicit when the call has insufficient evidence or when clarity matters:
package main
import "fmt"
type Factory struct{}
func (Factory) Zero[T any]() T {
var zero T
return zero
}
func main() {
factory := Factory{}
count := factory.Zero[int]()
labels := factory.Zero[[]string]()
fmt.Printf("count=%d labels=%v\n", count, labels)
}Zero has no value argument from which T could be inferred, so the call must supply [int] or another concrete type.
Receivers can be generic too
Receiver and method parameters are independent
A generic receiver introduces its own parameters, and a method may add more:
package main
import "fmt"
type Index[K comparable, V any] struct {
Values map[K]V
}
func (index Index[K, V]) Project[U any](fn func(K, V) U) []U {
result := make([]U, 0, len(index.Values))
for key, value := range index.Values {
result = append(result, fn(key, value))
}
return result
}
func main() {
stock := Index[string, int]{Values: map[string]int{"pens": 4}}
lines := stock.Project(func(name string, quantity int) string {
return fmt.Sprintf("%s: %d", name, quantity)
})
fmt.Println(lines)
}Method constraints may refer to receiver parameters already in scope. Here, the method accepts any edge type whose From method returns the graph's node type:
package main
import "fmt"
type Graph[N comparable] struct {
Nodes map[N]bool
}
type EdgeFrom[N any] interface {
From() N
}
func (g Graph[N]) ContainsOrigin[E EdgeFrom[N]](edge E) bool {
return g.Nodes[edge.From()]
}
type Route struct{ Origin string }
func (r Route) From() string { return r.Origin }
func main() {
g := Graph[string]{Nodes: map[string]bool{"north": true}}
fmt.Println(g.ContainsOrigin(Route{Origin: "north"}))
fmt.Println(g.ContainsOrigin(Route{Origin: "south"}))
}This relationship was expressible with a generic free function before Go 1.27. The new part is keeping it inside Graph's namespace.
Methods are values too
Capture an instantiated method
Selecting a generic method produces a generic method value. Instantiate it to store a normal function:
package main
import (
"fmt"
"strconv"
)
type Box[T any] struct{ Value T }
func (b Box[T]) Map[U any](fn func(T) U) Box[U] {
return Box[U]{Value: fn(b.Value)}
}
func main() {
box := Box[int]{Value: 27}
toText := box.Map[string]
result := toText(strconv.Itoa)
fmt.Println(result.Value)
}A method expression keeps the receiver as the first function argument:
package main
import "fmt"
type Scale int
func (s Scale) Apply[T ~int | ~float64](value T) T {
return T(s) * value
}
func main() {
applyFloat := Scale.Apply[float64]
fmt.Println(applyFloat(Scale(3), 2.5))
}The type of applyFloat is func(Scale, float64) float64. This is useful for adapters that accept functions but should not own a receiver instance yet.
The important boundary
Generic methods do not participate in interfaces
Interface methods still cannot declare type parameters:
type Mapper interface {
Map[T any](func(int) T) []T // compile error: interface methods cannot have type parameters
}A generic concrete method also does not implement a similarly shaped non-generic interface method:
type StringMapper interface {
Map(func(int) string) []string
}
type Numbers []int
func (n Numbers) Map[T any](fn func(int) T) []T { /* ... */ }
var _ StringMapper = Numbers{} // compile errorThe compiler cannot instantiate a concrete method implicitly to satisfy an interface. If interface dispatch is a requirement, keep a non-generic method with the exact interface signature or use a generic free function around the interface.
Reflection cannot expose uninstantiated generic methods
Reflection has no API for supplying type arguments, so generic methods are absent from reflected method sets. Ordinary methods remain visible:
package main
import (
"fmt"
"reflect"
)
type Toolkit struct{}
func (Toolkit) Name() string { return "toolkit" }
func (Toolkit) Wrap[T any](value T) []T { return []T{value} }
func main() {
typeOfToolkit := reflect.TypeFor[Toolkit]()
fmt.Println("visible methods:", typeOfToolkit.NumMethod())
for method := range typeOfToolkit.Methods() {
fmt.Println(method.Name)
}
fmt.Println((Toolkit{}).Wrap(27))
}Frameworks that discover behavior through reflection should continue to use ordinary methods, tags, or explicit registration.
The standard library uses the feature
math/rand/v2.Rand.N
Go 1.27 adds a generic method to *rand.Rand. It mirrors the top-level rand.N function while using a caller-owned random source:
generator := rand.New(rand.NewPCG(1, 2))
var retries uint16 = generator.N(uint16(10))
var shard int64 = generator.N(int64(64))
fmt.Printf("retries=%d shard=%d\n", retries, shard)The method preserves the integer type supplied by its argument. Before generic methods, this operation could exist only as a top-level generic function or as separate methods for each integer width.
Designing with generic methods
Prefer association over novelty
Generic methods are most useful when all of these are true:
- The operation clearly belongs to one concrete receiver type.
- It introduces a type not already available from the receiver.
- Static dispatch is enough; interface satisfaction is not required.
- The method name makes discovery easier than a package-level function would.
Keep a free function when the operation treats multiple inputs symmetrically, when no receiver is the obvious owner, or when a public package already has an established functional vocabulary.
The addition is source-compatible, but tools that parse or analyze Go code need Go 1.27 support before they can understand the syntax.
Quick reference
| Concept | Rule |
|---|---|
| Declaration | func (r Receiver) Method[T constraint](value T) |
| Receiver parameters | Available independently from method parameters |
| Inference | Uses ordinary arguments and callback types |
| Explicit instantiation | value.Method[Concrete] |
| Method value | Captures a receiver, then accepts ordinary arguments |
| Method expression | Keeps the receiver as the first function argument |
| Return type | May use both receiver and method type parameters |
| Interfaces | Cannot declare generic methods |
| Satisfaction | A generic method does not satisfy a non-generic interface method |
| Reflection | Generic methods are not exposed for dynamic invocation |
| Standard library | (*math/rand/v2.Rand).N is the first prominent example |
For the complete language rules, see the Go 1.27 specification and the Go 1.27 release notes.
Comments