Generic Methods

Learn how methods behave on generic types in Go, how to define them, and the key differences from generic functions.

Go added support for generics in version 1.18, introducing type parameters for functions and types. When people talk about “generic methods” in Go, they usually mean one of two things: methods defined on a generic type, or methods that introduce their own type parameters independent of the receiver. As of Go 1.22 (and through 1.26), only the first form exists. You can define methods on a parameterized type, but you cannot add fresh type parameters to a method of a non‑parameterized type.

This page explains how methods on generic types work, what you can and cannot do, and how to avoid the most common mistakes.

Methods on Generic Types

When you define a type with type parameters, like type List[T any] struct { ... }, you can write methods that use those type parameters. The receiver must list the type parameters exactly as they appear in the type declaration.

type List[T any] struct {
    head, tail *element[T]
}
type element[T any] struct {
    next *element[T]
    val  T
}
// Push adds a value to the end of the list.
func (lst *List[T]) Push(v T) {
    if lst.tail == nil {
        lst.head = &element[T]{val: v}
        lst.tail = lst.head
    } else {
        lst.tail.next = &element[T]{val: v}
        lst.tail = lst.tail.next
    }
}

Here Push is a method on the generic type List[T]. The type parameter T is declared on the type, and the method uses it in both the receiver and the parameter type v T. You don’t write func (lst *List[T]) Push[T any](v T) — that would be a syntax error. The type parameters live on the type, not on each individual method.

Everything compiles:

If your code looks like the example above and you see no complaints from the compiler, you’ve defined a method on a generic type correctly.

When you call a method on a generic type, you must instantiate the type first:

lst := List[int]{}
lst.Push(10)
lst.Push(13)
fmt.Println(lst.AllElements()) // [10 13]

The compiler knows that T is int, so Push expects an int argument. Type inference works here; you don’t need to write List[int]{}.Push[int](10).

Why This Works

A generic type is a template. List[int] and List[string] are two completely separate concrete types at compile time. When you write func (lst *List[T]) Push(v T), you’re essentially writing a method signature that will be stamped out for every concrete T. The method Push on List[int] only accepts int, and the method Push on List[string] only accepts string. They don’t share a single implementation that switches on type — Go generates (or shares via a unified dictionary, depending on the implementation) the code for each distinct type.

This is why the method cannot add type parameters that weren’t already on the type. The method only exists after the type is instantiated. If you tried to write func (lst *List[T]) Map[U any](fn func(T) U) List[U], you’d be asking the method to introduce a new type variable U that doesn’t belong to the receiver. The compiler rejects this.

Compiler error:

Attempting to define a method with its own type parameters on a non‑parameterized receiver will produce the error: method must have no type parameters. This applies even if the receiver is a generic type — the method cannot add parameters beyond those already on the type.

Repeating Type Parameters in the Receiver

You must repeat the type parameter list in the receiver exactly as it appears in the type declaration. Even if a method doesn’t use a specific type parameter, it still needs to appear in the receiver to keep the method signature valid for all instantiations.

type Pair[A, B any] struct {
    First  A
    Second B
}
// Both A and B must appear in the receiver, even though only A is used in the method body.
func (p Pair[A, B]) FirstValue() A {
    return p.First
}

If a type parameter isn’t used at all in a method, you can use the blank identifier _ to silence the compiler:

func (p Pair[A, _]) FirstValue() A {
    return p.First
}

This tells the compiler “I know B exists on the type, but this method doesn’t need it.” Using _ is optional; you can always list the full parameter set. It exists for readability and to avoid unused‑parameter warnings.

Don’t forget type parameters:

If you omit a type parameter in the receiver that’s part of the type’s parameter list, the compiler will complain that the method has a different number of type parameters than the type requires. The receiver must always list all type parameters of the base type, in the same order, even if they aren’t used.

Common Pitfalls

The biggest source of confusion is trying to write a generic method that introduces its own type parameters, like a Map or Filter on a slice. For example, you might want:

// THIS DOES NOT COMPILE
func (s []E) Map[V any](fn func(E) V) []V { ... }

This fails because []E is not a defined named type — you can’t declare methods on built‑in slice types. Even if you create a named slice type, you still can’t add a type parameter V to the method.

type Slice[T any] []T
// STILL DOES NOT COMPILE
func (s Slice[T]) Map[V any](fn func(T) V) []V { ... }

The method Map attempts to introduce V, which is not a type parameter of Slice. The Go specification explicitly says: a method can only have type parameters that are declared on the receiver’s base type. This restriction exists largely because of how interfaces work — generic methods would complicate interface satisfaction in ways that aren’t resolved yet.

Eagerly using generic methods as if they were methods on slices:

If you come from languages like Rust or C#, you may expect to be able to call mySlice.Map(...) directly. In Go, you’ll need a generic top‑level function instead: result := Map(mySlice, fn). This is a common friction point when first writing generic Go.

Workaround: Top‑Level Generic Functions

When you need behavior that depends on a type not part of the receiver, define a generic function instead of a method:

func Map[T, V any](s []T, fn func(T) V) []V {
    result := make([]V, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

You can then call Map(ints, strconv.Itoa) without specifying type arguments because the compiler infers them. This function works on any slice, not just a specific named type, and can introduce as many type parameters as you need.

Inference vs. explicit calls:

Generic functions support type inference. You can write Map(slice, fn) without [T, V] in most cases. However, if the compiler can’t infer all types, you’ll need to provide them explicitly.

The Future: True Generic Methods

The Go team has accepted a proposal to allow methods to introduce their own type parameters, separate from those on the receiver type. This is targeted for Go 1.27. Once that lands, you’ll be able to write:

func (s MySlice[T]) Map[V any](fn func(T) V) []V { ... }

The method Map would bring its own V, while T comes from the receiver’s type parameter. This will enable method chaining and more fluent APIs without sacrificing the benefits of generics.

Until then, the only form of “generic method” you can use is the one described in this document: methods that piggyback on the type parameters of a generic type.

Keep your Go version in mind:

If you’re reading this after Go 1.27 is released, the language’s capabilities will have expanded. Check the latest spec for the most up‑to‑date rules on generic method declarations.

Summary

Methods on generic types work by reusing the type parameters declared on the type itself. They do not introduce new type parameters, and you cannot add type parameters to a method of a non‑parameterized type or to a method on a built‑in type. When you need a function that works with multiple independent type parameters, reach for a generic top‑level function. The upcoming Go 1.27 release will eventually lift this restriction, allowing methods to declare their own type parameters alongside those from the receiver.