DEV Community

Cover image for Go 1.27 Has Generic Methods. They Can't Satisfy an Interface, and That's the Point
Gabriel Anhaia
Gabriel Anhaia

Posted on AI-assisted

Go 1.27 Has Generic Methods. They Can't Satisfy an Interface, and That's the Point


func GetAs[V any](c *Cache, key string) (V, bool) belongs on the cache. Until this year it had to live at package scope, because Go would not let a method declare its own type parameters.

Go 1.27, released in August 2026, changes that. The release notes call it "widely anticipated", and the standard library ships the first example: math/rand/v2 now has a method (*Rand) N[Int intType](Int) Int, next to the generic function N it already had. Calling it looks like any generic function call, with the type argument inferred from the argument:

r := rand.New(rand.NewPCG(1, 2))
backoff := r.N(500 * time.Millisecond) // Int = time.Duration
Enter fullscreen mode Exit fullscreen mode

The same paragraph of the release notes ends with the catch: "methods of interfaces may not declare type parameters nor can interface methods be implemented by generic methods."

That sentence will trip up anyone who reaches for an interface the moment they see a useful method.

A cache with a generic Get

Start with a module whose go.mod says go 1.27. The go directive sets the language version, and the compiler rejects features newer than it, so an older directive turns the method below into an error.

The cache stores values of any type. Before 1.27, typed retrieval needed a package-level helper. Now it is a method:

package main

import (
    "fmt"
    "sync"
)

type User struct {
    ID   int
    Plan string
}

// Cache stores values of any type under string keys.
type Cache struct {
    mu   sync.RWMutex
    data map[string]any
}

func NewCache() *Cache {
    return &Cache{data: make(map[string]any)}
}

func (c *Cache) Set(key string, v any) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = v
}

// Get returns the value under key as a V. ok is false
// when the key is missing or holds a different type.
func (c *Cache) Get[V any](key string) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[key].(V)
    return v, ok
}

func main() {
    c := NewCache()
    c.Set("user:42", User{ID: 42, Plan: "pro"})
    c.Set("hits", 17)

    u, ok := c.Get[User]("user:42")
    fmt.Println(u.Plan, ok) // u.Plan is "pro", ok is true

    _, ok = c.Get[string]("hits")
    fmt.Println(ok) // false: "hits" holds an int
}
Enter fullscreen mode Exit fullscreen mode

Cache itself is not generic: the spec allows a method to declare type parameters "possibly in addition to type parameters declared by the receiver specification", so a plain struct qualifies. And the call site must spell out [User], because V appears only in the results and there is nothing to infer it from. From the spec: "Like a generic function, a generic method must be instantiated before it can be called or used as a value."

The lock is a sync.RWMutex so concurrent readers don't block each other. The type assertion c.data[key].(V) handles both failure cases at once: a missing key yields a nil any, and the assertion on nil fails the same way a wrong type does.

Try to put Get in an interface

Now the reflex: you want to mock this cache in tests, so you write an interface for it.

// Attempt 1: rejected by the parser.
type TypedStore interface {
    Get[V any](key string) (V, bool)
    Set(key string, v any)
}
Enter fullscreen mode Exit fullscreen mode

This never reaches type checking. The compiler's parser has a dedicated message for it, "interface method must have no type parameters", and the spec states the rule directly: "Interface methods cannot declare type parameters, but they may use type parameters from the interface declaration."

Fine, you think: drop the type parameter and ask for the any version. c.Get[any] has exactly that signature.

// Attempt 2: the interface compiles, the assertion doesn't.
type Getter interface {
    Get(key string) (any, bool)
}

var _ Getter = (*Cache)(nil) // compile error
Enter fullscreen mode Exit fullscreen mode

The build fails because *Cache does not implement Getter. The Go blog post on generic methods uses this exact situation, where T.M[int] has the same signature as I.M, and explains why it still doesn't count: "interface implementation is a property of a (possibly instantiated) type, not of any particular method." The accepted proposal by Robert Griesemer puts it more bluntly: "a generic concrete method does not match against an interface method with the same name and signature."

So a generic method is invisible to interface satisfaction, in both directions. Interfaces can't ask for one, and a type can't use one to answer an interface.

Why the compiler says no

An interface value is two words: a pointer to a table and a pointer to the data. Russ Cox's write-up of Go's interface layout describes that table as metadata followed by "a list of function pointers", and a method call through the interface jumps through one of those pointers. One method, one slot, one compiled function.

A generic method compiles to a family of instantiations, and the compiler only generates the ones some call site asks for. When you call c.Get[User] on a concrete *Cache, the compiler sees User and builds that version.

A switchboard panel of identical round sockets beside a single cable whose end splits into many differently shaped plugs, none of which fits a socket
One slot per interface method, but a generic method has no single body to put in it.

Through an interface, that knowledge is gone. The code that calls s.Get[V] might live in a package compiled long after the one that defines *Cache, asking for a V nobody has seen. The blog post spells out the two ways out, and why Go took neither. Generating T.M "with every possible type argument" is "impractical". Passing the type argument at run time as a boxed value would let one compiled body serve every instantiation, "at the cost of indirect call overhead", and that cost would land on direct calls too.

The result is no hidden runtime cost on direct calls and no unbounded code generation. Generic methods are for organizing code on concrete types, and interfaces work exactly as before.

The refactor: keep the interface non-generic

The fix is to split the cache into two layers. The interface carries the non-generic primitive. The type-safe convenience sits on top of it, written once. Replace the Get from the first listing with the version below.

// Store is the contract. No type parameters anywhere.
type Store interface {
    Load(key string) (any, bool)
    Set(key string, v any)
}

func (c *Cache) Load(key string) (any, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[key]
    return v, ok
}

// Lookup works on any Store, not only *Cache.
func Lookup[V any](s Store, key string) (V, bool) {
    v, _ := s.Load(key)
    t, ok := v.(V)
    return t, ok
}

// Get replaces the earlier version: sugar over Lookup.
func (c *Cache) Get[V any](key string) (V, bool) {
    return Lookup[V](c, key)
}

var _ Store = (*Cache)(nil) // compiles
Enter fullscreen mode Exit fullscreen mode

*Cache satisfies Store because Load and Set are ordinary methods. The generic Get is still there, and it doesn't interfere, because it sits outside what interfaces can see. Your test fake implements two plain methods, and Lookup works against it unchanged.

A solid black foundation block with a slim burnt-orange adapter piece resting on top, many small shapes flowing through the adapter into the block
The interface holds the plain primitive. The generic layer sits above it.

If every value in a given cache has the same type, the other refactor is to move the type parameter from the method to the type. A type parameter on the type is different from one on the method. Generic interfaces like the one below have been legal since Go 1.18:

type Typed[V any] interface {
    Get(key string) (V, bool)
}

type TypedCache[V any] struct {
    mu   sync.RWMutex
    data map[string]V
}

func (c *TypedCache[V]) Get(key string) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.data[key]
    return v, ok
}

var _ Typed[User] = (*TypedCache[User])(nil)
Enter fullscreen mode Exit fullscreen mode

Here Get is not a generic method. V is fixed the moment you write TypedCache[User], so the method has one concrete signature and fits one interface slot. You pay with one cache per value type.

When a generic function is still the better choice

Generic methods fix the package-scope problem from the opening. The blog post's example is a Map method on a list, where chained calls read left to right instead of inside out. Generic methods still don't replace generic functions, and three cases favor the function.

The code holds an interface. A generic method lives on one concrete type. Lookup above works on every Store, including fakes and wrappers you haven't written yet. A method can't do that. The same goes for test doubles: in Go the usual seam for one is an interface, and an interface can only hold non-generic methods. Keep the generic part thin and put the behavior behind the plain method, as Load does above.

You need a tighter constraint than the type declares. The spec says receiver type parameter constraints "are implied by the receiver base type definition", so a method can't narrow them. A Sum over a List[E any] has to be a function:

type List[E any] []E

// A method can't add a numeric constraint to E.
func Sum[E int | int64 | float64](l List[E]) E {
    var s E
    for _, x := range l {
        s += x
    }
    return s
}
Enter fullscreen mode Exit fullscreen mode

Something discovers methods through reflection. The proposal says generic methods "won't be accessible via reflection (by name or index) for the same reason that uninstantiated generic functions are not accessible." Code that looks methods up dynamically through reflect will not find them.

Use the generic method when the operation belongs to one concrete type and every caller already has that type in hand. Use the function when the operation belongs to a contract.


If this was useful

Rules like this one feel arbitrary until you know the language underneath them. If the "why" section was the part you enjoyed, The Complete Guide to Go Programming goes deep on the language, the runtime, the standard library and concurrency.

Thinking in Go — the 2-book series on Go programming and hexagonal architecture

Top comments (1)

Collapse
 
dhruonweb profile image
Dhruv Malaviya •

The reason behind the restriction is worth stating, because it explains why this isn't a temporary gap. Interface satisfaction depends on a fixed method set , the itab is built from concrete signatures. A generic method has no single signature, it has one per instantiation, so there's nothing to put in the table.

That's also why allowing it would cost something real: implicit satisfaction works because the compiler compares signatures statically. Generic methods would force explicit declaration, which is a different language.

The practical consequence is the workaround people land on anyway , generic function at package scope when you need it behind an interface, method form when you're calling on a concrete type.

math/rand/v2 shipping (*Rand).N first is a good signal this is aimed at ergonomics rather than abstraction.