Generic Methods (Go 1.27)
Generic Methods (Go 1.27)
Thesis
Since Go 1.18 a method could use its receiver’s type parameters (func (s *Stack[T]) Push(v T)) but could not declare new ones. Every operation that needed a second type, such as turning a Box[int] into a Box[string], had to become a package-level function. Go 1.27 removes that restriction: a concrete method may declare its own type parameter list.
The feature is deliberately narrow. Interfaces still cannot declare generic methods, and a generic method never satisfies an interface method. Generic methods are a namespacing and ergonomics tool, not a new kind of polymorphism. This chapter shows exactly what compiles, what the compiler says when it does not, and how to decide between a generic method and a generic function.
The short tour in Methods and Receivers introduces the syntax. This chapter is the deep version.
Verified with Go 1.27.2 (linux/amd64) on 2026-10-11. Primary sources: Go 1.27 release notes, Generic Methods (Go blog), proposal #77273.
Learning goals
- Declare a method with its own type parameters on generic and non-generic receivers
- Call it with inference, with explicit instantiation, as a method value and as a method expression
- Read the five compiler errors you will hit first
- Explain why generic methods cannot satisfy interfaces, and design around it
- Choose between a generic method and a generic function
Every listing in this chapter
Each listing is a complete main.go in its own directory with this go.mod:
module example.com/<dir>
go 1.27
Run it with go run .. The go 1.27 line matters, as the next section shows.
Case 1: the canonical Map
// main.go
package main
import (
"fmt"
"strconv"
)
type Box[T any] struct {
Value T
}
// Map declares its own type parameter U, on top of the receiver's T.
func (b Box[T]) Map[U any](f func(T) U) Box[U] {
return Box[U]{Value: f(b.Value)}
}
func main() {
n := Box[int]{Value: 42}
s := n.Map(strconv.Itoa) // U inferred as string
q := n.Map[float64](func(v int) float64 { return float64(v) / 8 })
fmt.Printf("%T %q\n", s, s.Value)
fmt.Printf("%T %v\n", q, q.Value)
// Calls chain, and each link can change the type.
l := n.Map(strconv.Itoa).Map(func(s string) int { return len(s) })
fmt.Printf("%T %v\n", l, l.Value)
}go run .main.Box[string] "42"
main.Box[float64] 5.25
main.Box[int] 2
Three things to notice:
- The receiver list
Box[T]names the type’s parameter. The method’s own list[U any]comes after the method name, exactly where a function’s list goes. Uis inferred from the argument (strconv.Itoaisfunc(int) string), so most calls need no brackets.- Explicit instantiation is
n.Map[float64](...): the type argument attaches to the method, not to the receiver.
The chain on the last line was the motivating case for the proposal. Before 1.27 it read inside-out as Map(Map(n, strconv.Itoa), lenFn).
Case 2: the go directive gates the feature
Copy the same file into a module whose go.mod says go 1.26 and build with the 1.27 toolchain:
go build .# example.com/old
./main.go:14:21: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)
The language version comes from go.mod (or a //go:build go1.27 constraint on the file), not from the toolchain you happen to run. Libraries that want to keep supporting 1.26 users cannot adopt generic methods until they raise their go line.
Case 3: method values, method expressions and inference from the target
// main.go
package main
import (
"fmt"
"strconv"
)
type Box[T any] struct{ Value T }
func (b Box[T]) Map[U any](f func(T) U) Box[U] { return Box[U]{f(b.Value)} }
func main() {
b := Box[int]{Value: 7}
mv := b.Map[string] // method value: receiver bound, U fixed
me := Box[int].Map[string] // method expression: receiver becomes arg 1
fmt.Println(mv(strconv.Itoa).Value, me(b, strconv.Itoa).Value)
// Go 1.27 also infers U from the target function type.
var f func(func(int) string) Box[string] = b.Map
fmt.Println(f(strconv.Itoa).Value)
}7 7
7
The last assignment relies on the other 1.27 language change: function type inference now works in every assignment context (variables, composite literals, conversions, channel sends), as covered in Generic Functions. When there is no target type and no argument, there is nothing to infer from:
// main.go
package main
type Box[T any] struct{ Value T }
func (b Box[T]) Map[U any](f func(T) U) Box[U] { return Box[U]{f(b.Value)} }
func main() {
b := Box[int]{Value: 7}
f := b.Map // nothing to infer U from
_ = f
}# example.com/uninst
./main.go:10:7: cannot use generic function b.Map without instantiation
Case 4: what interfaces cannot do
An interface method may not declare type parameters. Even gofmt refuses to parse it:
// main.go
package main
type Mapper interface {
Map[U any](f func(int) U) U
}
func main() {}# example.com/ifacedecl
./main.go:5:5: interface method must have no type parameters
./main.go:5:25: undefined: U
A generic method also never implements a non-generic interface method, even when it could be instantiated to match:
// main.go
package main
import "fmt"
type Stringer interface{ String() string }
type Opt struct{}
func (Opt) String[T any]() string { var z T; return fmt.Sprint(z) }
func main() {
var _ Stringer = Opt{}
}# example.com/ifaceimpl
./main.go:13:19: cannot use Opt{} (value of struct type Opt) as Stringer value in variable declaration: Opt does not implement Stringer (wrong type for method String)
have String[T any]() string
want String() string
Why the line is drawn there
Packages are compiled separately. When main passes a T to some function p.F(i I) in another package, the compiler cannot know which methods p.F will call, so it generates code for every non-generic method of T where the type is declared or instantiated. That guarantees the method table behind the interface value can be filled in.
If I could declare M[P any](), p.F might call i.M[int](), i.M[string]() or any other instantiation, and none of them are visible while main is being compiled. The compiler would have to instantiate T.M for every possible type argument, which is impossible with Go’s per-instantiation code generation. Boxing the type arguments behind their constraint interfaces would let every instantiation share one body, but every call would then be indirect, including direct calls that never touch an interface. The Go blog post walks through this argument. Go 1.27 therefore treats concrete generic methods and generic interface methods as separate questions, and answers only the first.
Note what follows. T{}.M[int]() has the signature func(), but T still does not implement interface{ M() }. Interface satisfaction is a property of the type’s declared methods, and T declares the uninstantiated M[P any], not M[int].
You can see the same split in reflection: generic methods are not part of the runtime method set.
// main.go
package main
import (
"fmt"
"reflect"
)
type Reg struct{ items map[string]any }
func (r *Reg) Name() string { return "reg" }
func (r *Reg) Put[T any](k string, v T) { r.items[k] = v }
func main() {
t := reflect.TypeFor[*Reg]()
fmt.Println("NumMethod:", t.NumMethod())
for i := range t.NumMethod() {
fmt.Println(" -", t.Method(i).Name)
}
}NumMethod: 1
- Name
Code that discovers methods by reflection, such as RPC registries, template call and some DI containers, will not see generic methods.
Case 5: the receiver’s parameters are already in scope
The method’s type parameter list shares a scope with the receiver’s, so names must not collide:
// main.go
package main
type Box[T any] struct{ Value T }
func (b Box[T]) Conv[T any]() T { var z T; return z }
func main() {}# example.com/shadow
./main.go:6:22: T redeclared in this block
./main.go:6:13: other declaration of T
Pick a different letter (U, R, K) or a descriptive name.
Case 6: generic methods on non-generic types
The receiver does not have to be generic. This is the most useful shape in application code: a client or store that owns configuration and state, with the result type chosen per call.
// main.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
)
type Client struct {
base string
hc *http.Client
}
// GetJSON is a generic method: the decode target belongs to the call,
// the base URL and transport belong to the Client.
func (c *Client) GetJSON[T any](path string) (T, error) {
var out T
resp, err := c.hc.Get(c.base + path)
if err != nil {
return out, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
return out, fmt.Errorf("GET %s: %s: %s", path, resp.Status, bytes.TrimSpace(b))
}
err = json.NewDecoder(resp.Body).Decode(&out)
return out, err
}
type Health struct {
Status string `json:"status"`
}
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"status":"ok"}`)
})
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `[{"id":1,"name":"ada"},{"id":2,"name":"ken"}]`)
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := &Client{base: srv.URL, hc: srv.Client()}
h, err := c.GetJSON[Health]("/health")
fmt.Println(h, err)
users, err := c.GetJSON[[]User]("/users")
fmt.Println(users, err)
_, err = c.GetJSON[Health]("/missing")
fmt.Println(err)
}{ok} <nil>
[{1 ada} {2 ken}] <nil>
GET /missing: 404 Not Found: 404 page not found
T appears only in the result, so it can never be inferred. Calls always spell GetJSON[Health]. That is a feature here: the wire type is visible at the call site.
The standard library made the same move. math/rand/v2 gained (*Rand).N, a generic method that mirrors the package-level rand.N:
// main.go
package main
import (
"fmt"
"math/rand/v2"
"time"
)
func main() {
r := rand.New(rand.NewPCG(1, 2))
var d time.Duration = r.N(5 * time.Second) // Int inferred as time.Duration
var b uint8 = r.N(uint8(200))
fmt.Printf("%T %v\n", d, d)
fmt.Printf("%T %v\n", b, b)
}time.Duration 3.846866346s
uint8 123
The output is deterministic because the PCG source is seeded. Before 1.27 a seeded *Rand offered only IntN, Int32N, Int64N, UintN and so on. A time.Duration jitter needed a conversion dance or the global, unseeded rand.N.
Case 7: promotion through embedding
Generic methods are promoted from embedded fields like any other method:
// main.go
package main
import "fmt"
type Store struct{ items map[string]any }
func (s *Store) Put[T any](k string, v T) { s.items[k] = v }
// Lookup stays a function: it needs T only in the result.
func Lookup[T any](s *Store, k string) (T, bool) {
v, ok := s.items[k].(T)
return v, ok
}
type Cache struct {
*Store
hits int
}
func main() {
c := Cache{Store: &Store{items: map[string]any{}}}
c.Put("port", 8080) // promoted generic method, T = int
c.Put("host", "localhost")
p, ok := Lookup[int](c.Store, "port")
fmt.Println(p, ok)
_, ok = Lookup[string](c.Store, "port")
fmt.Println(ok)
}8080 true
false
Lookup could equally be a generic method (s *Store) Get[T any](k string) (T, bool). The choice is about where readers will look for it, which is the topic of the next section.
Case 8: instantiation cycles
Map returns Box[U] for a fresh U, which is fine. A method whose result is the same generic type instantiated with something built from T is rejected:
// main.go
package main
import "fmt"
type Box[T any] struct{ Value T }
type pairOf[A, B any] = struct {
A A
B B
}
// Zip's result type is built from T, so Box[T] would need Box[pairOf[T, U]],
// which would need Box[pairOf[pairOf[T, U], V]], and so on.
func (b Box[T]) Zip[U any](o Box[U]) Box[pairOf[T, U]] {
return Box[pairOf[T, U]]{pairOf[T, U]{b.Value, o.Value}}
}
func main() { fmt.Printf("%+v\n", Box[int]{1}.Zip(Box[string]{"x"})) }# example.com/zip
./main.go:6:10: instantiation cycle:
./main.go:15:42: T instantiated as pairOf[T, U]
This rule predates 1.27. A plain method func (b Box[T]) Wrap() Box[[]T] fails the same way with T instantiated as []T. Generic methods make the shape more tempting to write, though. The fix is to return a different generic type:
// main.go
package main
import "fmt"
type Box[T any] struct{ Value T }
type Pair[A, B any] struct {
First A
Second B
}
// Zip returns a different generic type, so there is no cycle.
func (b Box[T]) Zip[U any](o Box[U]) Pair[T, U] {
return Pair[T, U]{b.Value, o.Value}
}
func main() {
fmt.Printf("%+v\n", Box[int]{1}.Zip(Box[string]{"x"}))
}{First:1 Second:x}
Designing with generic methods
The interface seam pattern
Because generic methods cannot sit in interfaces, keep the interface non-generic and put the typed convenience on top as a generic function. Tests fake the small interface, and callers get type safety:
// main.go
package main
import (
"encoding/json"
"fmt"
"strings"
)
// Source is the interface: non-generic, so any type can implement it
// and it can be faked in tests.
type Source interface {
Fetch(key string, dst any) error
}
// Get is the typed convenience, written once, on top of the interface.
func Get[T any](s Source, key string) (T, error) {
var v T
err := s.Fetch(key, &v)
return v, err
}
type mapSource map[string]string
func (m mapSource) Fetch(key string, dst any) error {
raw, ok := m[key]
if !ok {
return fmt.Errorf("fetch %q: missing", key)
}
return json.NewDecoder(strings.NewReader(raw)).Decode(dst)
}
func main() {
src := mapSource{"limits": `{"rps":200,"burst":50}`, "hosts": `["a","b"]`}
limits, err := Get[map[string]int](src, "limits")
fmt.Println(limits, err)
hosts, err := Get[[]string](src, "hosts")
fmt.Println(hosts, err)
_, err = Get[int](src, "port")
fmt.Println(err)
}map[burst:50 rps:200] <nil>
[a b] <nil>
fetch "port": missing
If Get were a generic method on a concrete client type, every consumer that wanted to fake the client would lose it, because the method cannot appear in the consumer’s interface.
Method or function?
| Situation | Prefer |
|---|---|
Operation is about this type and readers will look for it in its method list (Box.Map, Rand.N, Client.GetJSON) |
Generic method |
You want call chains (q.Filter(...).Map(...)) |
Generic method |
| Consumers will want to mock or swap the type behind an interface | Non-generic interface method plus a generic function (seam pattern) |
Operation works across several types or takes two unrelated receivers (slices.Equal, maps.Copy) |
Generic function |
Something must find it via reflect (RPC, templates) |
Non-generic method |
You must keep supporting go 1.26 users |
Generic function |
Cost model
A generic method is instantiated like a generic function, using the same GC-shape stenciling with dictionaries, and no new runtime machinery is involved. Two instantiations with the same shape (for example, two pointer types) share code. Instantiations with different shapes get separate code. Generics Implementation covers the mechanism. There is no interface dispatch to devirtualize, because generic methods are never called through an interface.
The trap
- Designing a generic method into a type you meant to mock. The method cannot appear in an interface, so the type cannot be faked behind one. Use the seam pattern.
- Forgetting the
goline. Generic methods compile only in files whose language version is 1.27 or later. A 1.27 toolchain does not overridego 1.26ingo.mod. - Expecting
reflectto see them.NumMethod,MethodByNameandtext/templatemethod calls ignore generic methods. - Reusing the receiver’s parameter name.
func (b Box[T]) M[T any]()is a redeclaration error. - Returning
Box[f(T)]from a method ofBox[T]. That is an instantiation cycle. Return a different type, such asPair[T, U]. - Result-only type parameters.
Tthat appears only in results is never inferred, so every call needs explicit brackets. That is fine forGetJSON[T], but noisy for anything called in a loop.
The boring rule
Add a generic method when the operation clearly belongs to the type and nobody needs to see it through an interface. Otherwise keep the interface small and non-generic and write the generic helper as a function.
Try this
- Give
Box[T]a methodFlatMap[U any](f func(T) Box[U]) Box[U]and chain it withMap. - Write a
Pipelinetype withThen[U any](func(T) (U, error))that short-circuits on the first error. - Turn
Lookupin Case 7 into a method and decide which reads better at the call site.
Sources
- Go 1.27 release notes, language changes: https://go.dev/doc/go1.27
- Go blog, “Generic Methods”: https://go.dev/blog/generic-methods
- Go blog, “Go 1.27 is released”: https://go.dev/blog/go1.27
- Proposal: spec: generic methods for Go, #77273: https://github.com/golang/go/issues/77273
- Earlier requests #49085 and #50981, linked from the proposal
math/rand/v2(*Rand).N: https://pkg.go.dev/math/rand/v2#Rand.N