Methods on Generic Types

Learn how to define methods on generic types in Go, including receiver type parameters and the restrictions that apply.

A generic type is a type that depends on one or more type parameters—placeholders you fill in with concrete types when you use the type. When you define a method on a generic type, the method’s receiver must carry those same type parameters so the code inside the method knows which types it is working with. This page explains exactly how to write those methods, what the compiler enforces, and the most common mistakes you will hit.

A Quick Look at Generic Types

Before writing methods, you need a generic type. Here is a small generic Pair that holds two values of potentially different types:

type Pair[A, B any] struct {
    First  A
    Second B
}

A and B are type parameters. The constraint any means “any type is allowed.” You would create a concrete pair like Pair[string, int]{First: "age", Second: 30}. The compiler replaces A with string and B with int for that particular instance.

How to Define a Method on a Generic Type

A method on a generic type must declare the type parameters in the receiver. The syntax looks like an instantiation of the receiver type, using identifiers that name the type parameters for the method body.

// Swap returns a new Pair with the two fields exchanged.
func (p Pair[A, B]) Swap() Pair[B, A] {
    return Pair[B, A]{First: p.Second, Second: p.First}
}

Here the receiver is (p Pair[A, B]). The names A and B match the type parameters from the Pair definition. You could choose different names—X and Y, for example—as long as they are consistent across the receiver and the method body. Inside Swap, A and B refer to the concrete types the caller used when creating the pair.

Everything works if the type parameters match:

When you write func (p Pair[A, B]) ... on a Pair[A, B any] definition, the compiler accepts it immediately and your method can use A and B naturally. If you later call p := Pair[string, int]{...} then p.Swap() will return a Pair[int, string].

The method signature must include all type parameters the base type was defined with, even if the method does not use a particular one. If you have a type type Container[T, U any] struct { ... } and you write a method that only needs T, you still must list both:

func (c Container[T, U]) OnlyUsesT() T {
    var zero T
    return zero
}

Attempting to omit U results in a compile error: “generic type Container[T, U any] requires 2 type arguments.”

Missing type parameters in the receiver breaks compilation:

If you write func (c Container[T]) ... on a type defined with two type parameters, the compiler will report that the type parameter count does not match. The error message is explicit, but beginners often miss it because they think only the parameters they need must appear.

Why Receiver Type Parameters Are Necessary

When you call a method on a generic type, Go needs to know the concrete types for every type parameter so it can generate the correct machine code. The receiver carries that information. Internally, each distinct instantiation of a generic type—say Pair[string, int] and Pair[float64, bool]—has its own method set, and the compiler creates separate implementations for each combination actually used in your program.

This design keeps method dispatch simple and predictable. The receiver already tells the compiler which concrete types the method body must work with. There is no runtime type switching or boxing involved.

The Restriction: No Extra Type Parameters on the Method

A common desire is to add a type parameter that exists only on the method, not on the type itself. For example, you might want a Map method that transforms a Pair[A, B] into a Pair[V, B] without adding V to the type definition. That is not allowed in current Go.

// This will NOT compile.
func (p Pair[A, B]) MapFirst[V any](fn func(A) V) Pair[V, B] {
    return Pair[V, B]{First: fn(p.First), Second: p.Second}
}

The compiler will stop with: “method must have no type parameters.” The language specification restricts methods to use only the type parameters declared on the receiver’s base type. You cannot introduce new type parameters in a method signature.

Workaround changes the type’s identity:

If you really need a method with an extra type parameter, you can lift that parameter to the type level: type Pair[A, B, V any] struct { … }. However, this makes every instance carry that extra parameter, even when it is irrelevant. You lose the clean abstraction, and all callers must supply a type argument for V. This is rarely the right design. Instead, write a standalone generic function that takes the receiver as a normal argument.

When You Might Want a Standalone Function Instead

A generic function that operates on your type is often the cleanest alternative. It gives you the extra type parameter and, importantly, benefits from type inference at the call site—something methods on generic types cannot currently do for new type parameters.

// MapFirst is a standalone function that takes a Pair and returns a new Pair.
func MapFirst[A, B, V any](p Pair[A, B], fn func(A) V) Pair[V, B] {
    return Pair[V, B]{First: fn(p.First), Second: p.Second}
}

Callers can write MapFirst(p, strings.ToUpper) without specifying any type arguments because the compiler infers A, B, and V from the arguments. This is the idiomatic way to handle operations that introduce new type parameters.

Common Mistakes When Writing Methods on Generic Types

Forgetting to repeat all type parameters

type Store[K comparable, V any] struct {
    data map[K]V
}
// Compile error: generic type Store[K comparable, V any] requires 2 type arguments
func (s Store[K]) Get(key K) (V, bool) {
    v, ok := s.data[key]
    return v, ok
}

Every type parameter from the type definition must appear in the receiver, exactly once.

Using the wrong constraint names

The receiver’s type parameter names are local to the method. You can rename them, but the constraints are inherited from the type definition. You cannot tighten or loosen them in the receiver syntax.

type Numbers[T int | float64] struct {
    val T
}
// This will not compile—'any' is not the original constraint.
func (n Numbers[T any]) Double() T {
    return n.val * 2
}

The method signature cannot override the constraint. The compiler will complain that * is not defined for the any constraint. You must keep the original constraint, even if you rename the parameter.

Attempting to call a method without concrete type arguments on the type

Methods belong to specific instantiations. You cannot call a method on a generic type name itself; you must first create a value of a concrete instantiation.

// Wrong: Pair.Swap() is meaningless.
p := Pair.Swap()

Always create a value: p := Pair[string, int]{...} then call p.Swap().

A Real-World Example: A Generic Stack

A stack is a classic data structure where you can push items onto the top and pop them off. A generic version works with any element type while keeping type safety.

type Stack[T any] struct {
    items []T
}
// Push adds an item to the top of the stack.
func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}
// Pop removes and returns the top item. It returns the zero value of T
// if the stack is empty, along with a boolean indicating success.
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    idx := len(s.items) - 1
    item := s.items[idx]
    s.items = s.items[:idx]
    return item, true
}
// Peek returns the top item without removing it.
func (s *Stack[T]) Peek() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    return s.items[len(s.items)-1], true
}

All three methods receive *Stack[T]. The type parameter T flows from the stack instance you create. When you write s := Stack[int]{}, every method automatically knows T is int, and Pop returns an int.

You can use the stack like this:

func main() {
    s := Stack[string]{}
    s.Push("hello")
    s.Push("world")
    top, ok := s.Peek()
    fmt.Println(top, ok) // Output: world true
    for {
        val, hasMore := s.Pop()
        if !hasMore {
            break
        }
        fmt.Println(val)
    }
    // Output:
    // world
    // hello
}

The method bodies are type-safe. You cannot accidentally push an int onto a Stack[string]. The compiler catches that mismatch.

Pointer receiver is typical for methods that mutate the type:

Push and Pop change the items slice, so the receiver is a pointer *Stack[T]. If you used a value receiver, the changes would not persist because the receiver would be a copy. This rule is the same as for non-generic types.

Summary

Methods on generic types give you the same structured, type-safe behavior you expect from regular methods, extended to parameterized types. You declare them by repeating the type parameters in the receiver, which makes those concrete types available inside the method body. The compiler enforces that you cannot introduce new type parameters on a method—only those belonging to the type definition are allowed. For operations that need extra type parameters, a standalone generic function is the idiomatic and often cleaner choice.