Go has supported generics since Go 1.18, allowing developers to write reusable generic functions and types without duplicating implementations for different data types. However, one important limitation remained: methods could not declare their own type parameters.
Go 1.27 changes that.
With Go 1.27, concrete methods can declare type parameters independently of the type parameters already declared by their receiver. This makes certain APIs more expressive, especially when a method transforms one type into another.
For example, a generic collection can now expose a method like this:
type List[E any] []E
func (l List[E]) Map[R any](fn func(E) R) List[R] {
result := make(List[R], len(l))
for i, value := range l {
result[i] = fn(value)
}
return result
}
The List type is generic over E, while the Map method introduces its own R type parameter.
This article explains where generic methods improve real Go APIs, where ordinary generic functions are still a better choice, and why generic methods do not automatically make a type compatible with a generic interface.
What Are Generic Methods in Go 1.27?
Before Go 1.27, Go allowed type parameters on functions and type declarations, but not directly on methods.
You could write a generic type:
type Box[T any] struct {
Value T
}
And you could write a generic function:
func Convert[T, R any](value T, fn func(T) R) R {
return fn(value)
}
But you could not write a method that introduced a new type parameter:
// Supported in Go 1.27
func (b Box[T]) Convert[R any](fn func(T) R) R {
return fn(b.Value)
}
Go 1.27 introduces this capability for concrete methods.
The receiver's type parameters and the method's type parameters have different roles.
type Box[T any] struct {
Value T
}
func (b Box[T]) Convert[R any](fn func(T) R) R {
return fn(b.Value)
}
Here:
Tbelongs toBox.Rbelongs toConvert.Tdescribes the value already stored in the receiver.Rdescribes the result type produced by the method.
That distinction is where generic methods become particularly useful.
A Simple Generic Method Example
Consider a generic List:
type List[T any] []T
Suppose the list contains integers, but you want to transform each integer into a string.
Before Go 1.27, a common solution was a generic function:
func Map[T, R any](items List[T], fn func(T) R) List[R] {
result := make(List[R], len(items))
for i, item := range items {
result[i] = fn(item)
}
return result
}
Usage:
numbers := List[int]{10, 20, 30}
strings := Map(numbers, func(n int) string {
return fmt.Sprintf("Value-%d", n)
})
This works, but Map exists at package scope.
With Go 1.27, the operation can belong directly to List:
func (l List[T]) Map[R any](fn func(T) R) List[R] {
result := make(List[R], len(l))
for i, item := range l {
result[i] = fn(item)
}
return result
}
Now the call becomes:
numbers := List[int]{10, 20, 30}
strings := numbers.Map(func(n int) string {
return fmt.Sprintf("Value-%d", n)
})
The API communicates the relationship more naturally:
numbers -> Map -> strings
instead of:
Map(numbers) -> strings
Why Generic Methods Are Useful
The biggest benefit is not simply that fewer lines of code are required.
The more important benefit is API organization.
A method naturally associates behavior with the type it operates on.
Consider an application with several collection-like types:
type List[T any] []T
type Set[T comparable] map[T]struct{}
type Queue[T any] []T
If every generic operation is implemented as a package-level function, the package can quickly accumulate names such as:
MapList(...)
FilterList(...)
ReduceList(...)
MapSet(...)
FilterSet(...)
MapQueue(...)
Generic methods allow operations that conceptually belong to a type to remain attached to that type.
For example:
func (l List[T]) Map[R any](fn func(T) R) List[R] {
// ...
}
func (l List[T]) Filter(fn func(T) bool) List[T] {
// ...
}
The API becomes easier to discover:
items.Map(...)
items.Filter(...)
This is especially useful in libraries where discoverability and method chaining matter.
Generic Methods and Method Chaining
One of the strongest practical use cases is transforming values through multiple operations.
Suppose you have:
type List[T any] []T
You can define:
func (l List[T]) Map[R any](fn func(T) R) List[R] {
result := make(List[R], len(l))
for i, item := range l {
result[i] = fn(item)
}
return result
}
Now transformations can be chained:
numbers := List[int]{2, 4, 6, 8}
result := numbers.
Map(func(n int) float64 {
return float64(n) / 2
}).
Map(func(n float64) string {
return fmt.Sprintf("%.1f", n)
})
The type changes at every Map operation:
List[int]
|
| Map
v
List[float64]
|
| Map
v
List[string]
This is difficult to express as naturally when every operation must be a package-level function.
Generic Method vs Generic Function
Both approaches are still valid.
The important question is which API better represents the relationship between the operation and the data.
Generic function
func Map[T, R any](items List[T], fn func(T) R) List[R] {
result := make(List[R], len(items))
for i, item := range items {
result[i] = fn(item)
}
return result
}
Usage:
result := Map(numbers, transform)
Generic method
func (l List[T]) Map[R any](fn func(T) R) List[R] {
result := make(List[R], len(l))
for i, item := range l {
result[i] = fn(item)
}
return result
}
Usage:
result := numbers.Map(transform)
The implementation can be almost identical, but the API semantics are different.
Concern | Generic Function | Generic Method |
|---|---|---|
Operation belongs to a type | Less explicit | Very explicit |
Method chaining | Less natural | Natural |
Package-level namespace | More crowded | Cleaner |
Reuse across unrelated types | Excellent | More specialized |
Discoverability through IDE | Good | Often better |
Works without a receiver | Yes | No |
Interface-based design | Can be easier | Has important limitations |
Best for transformations on a type | Good | Often better |
Where Generic Methods Simplify Real APIs
Generic methods are particularly useful when the receiver represents the primary object being transformed.
Collection transformations
Collections are one of the clearest examples.
type List[T any] []T
func (l List[T]) Map[R any](fn func(T) R) List[R] {
result := make(List[R], len(l))
for i, item := range l {
result[i] = fn(item)
}
return result
}
func (l List[T]) Filter(fn func(T) bool) List[T] {
result := make(List[T], 0, len(l))
for _, item := range l {
if fn(item) {
result = append(result, item)
}
}
return result
}
Usage:
users := List[User]{
{Name: "Alice", Active: true},
{Name: "Bob", Active: false},
{Name: "Charlie", Active: true},
}
activeNames := users.
Filter(func(u User) bool {
return u.Active
}).
Map(func(u User) string {
return u.Name
})
The receiver remains the conceptual center of the operation.
Data conversion
Generic methods can also be useful when an object can be converted into different representations.
type Result[T any] struct {
Value T
}
func (r Result[T]) Map[R any](fn func(T) R) Result[R] {
return Result[R]{
Value: fn(r.Value),
}
}
Usage:
result := Result[int]{Value: 42}
converted := result.Map(func(value int) string {
return fmt.Sprintf("ID-%d", value)
})
This keeps the transformation associated with the result object.
Builder-like APIs
Some builder APIs can also benefit when each operation produces a different type.
type Value[T any] struct {
Data T
}
func (v Value[T]) Convert[R any](fn func(T) R) Value[R] {
return Value[R]{
Data: fn(v.Data),
}
}
This is useful when a transformation is conceptually part of the value's API rather than a general-purpose utility.
Where Generic Methods Do Not Help Much
Generic methods are not a replacement for generic functions.
Consider a general sorting helper:
func Sort[T cmp.Ordered](items []T) {
slices.Sort(items)
}
There may be no meaningful receiver type that owns this behavior.
A package-level generic function remains a clean choice.
The same applies to utilities such as:
func Min[T cmp.Ordered](a, b T) T
or:
func Clamp[T cmp.Ordered](value, min, max T) T
These operations do not naturally belong to a particular receiver.
A useful rule is:
If the operation is fundamentally about a value's type, consider a generic function. If the operation is behavior naturally owned by an object, consider a generic method.
Generic Methods Do Not Create Generic Interface Methods
This is one of the most important limitations in Go 1.27.
You can write:
type Processor struct{}
func (Processor) Process[T any](value T) T {
return value
}
But you cannot define an interface containing a generic method:
// Not valid Go 1.27
type ProcessorInterface interface {
Process[T any](value T) T
}
Go 1.27 supports generic methods on concrete types, but interface methods cannot declare their own type parameters.
This distinction matters significantly when designing public APIs.
Why Generic Methods Cannot Implement Generic Interfaces
Consider this hypothetical design:
type Transformer interface {
Transform[T any](value T) T
}
A concrete implementation might look like:
type MyTransformer struct{}
func (MyTransformer) Transform[T any](value T) T {
return value
}
The problem is that Go's interface dispatch model would need to determine which instantiations of the generic method could be called through the interface.
The set of possible type arguments is open-ended.
For example:
transformer.Transform[int](10)
transformer.Transform[string]("hello")
transformer.Transform[User](user)
A compiler cannot simply assume a finite list of method instantiations when an interface can cross package boundaries.
Therefore, Go 1.27 deliberately supports generic concrete methods without introducing generic interface methods.
Generic Methods Still Work With Ordinary Interfaces
This limitation does not mean generic methods are incompatible with interfaces in general.
Suppose:
type Stringer interface {
String() string
}
A type can have both ordinary interface methods and additional generic methods:
type Box[T any] struct {
Value T
}
func (b Box[T]) String() string {
return fmt.Sprint(b.Value)
}
func (b Box[T]) Convert[R any](fn func(T) R) R {
return fn(b.Value)
}
Box[T] can satisfy Stringer because String() is an ordinary method.
The generic Convert method simply exists as additional concrete functionality.
Method Expressions With Generic Methods
Generic methods can also be used as method expressions.
Consider:
type List[T any] []T
func (l List[T]) Map[R any](fn func(T) R) List[R] {
result := make(List[R], len(l))
for i, item := range l {
result[i] = fn(item)
}
return result
}
You can obtain a method expression for a specific instantiation:
mapIntsToStrings := List[int].Map[string]
Then use it as a function:
numbers := List[int]{1, 2, 3}
result := mapIntsToStrings(numbers, func(n int) string {
return fmt.Sprintf("%d", n)
})
This is useful when an API expects a function rather than a method call.
It also demonstrates an important point: generic methods are not restricted to direct method invocation.
Type Inference Makes Generic Methods Easier to Use
You do not always have to specify the method's type argument manually.
Given:
func (l List[T]) Map[R any](fn func(T) R) List[R] {
result := make(List[R], len(l))
for i, item := range l {
result[i] = fn(item)
}
return result
}
Go can infer R from the transformation function:
names := numbers.Map(func(n int) string {
return strconv.Itoa(n)
})
Here:
T = int
R = string
You can also specify the type argument explicitly when needed:
names := numbers.Map[string](strconv.Itoa)
In normal application code, inference usually produces cleaner APIs.
A Practical API Design Example
Suppose you are designing a query result abstraction.
type QueryResult[T any] struct {
Data T
Error error
}
A transformation operation can be expressed as:
func (r QueryResult[T]) Map[R any](fn func(T) R) QueryResult[R] {
if r.Error != nil {
return QueryResult[R]{
Error: r.Error,
}
}
return QueryResult[R]{
Data: fn(r.Data),
}
}
Now an API response can be transformed without repeatedly unpacking and rebuilding the result.
userResult := QueryResult[User]{
Data: user,
}
response := userResult.Map(func(u User) UserResponse {
return UserResponse{
ID: u.ID,
Name: u.Name,
}
})
This pattern is useful because the transformation is logically part of the QueryResult abstraction.
However, this should not automatically become a reason to put every generic operation on the type.
A large method surface can make an API harder to understand.
Common Mistakes
Trying to put generic methods into interfaces
This is not supported:
type Mapper interface {
Map[R any](func(int) R) R
}
If your design requires polymorphism over generic operations, reconsider the abstraction.
A generic function may be a better fit.
Using a generic method when a function is clearer
This:
service.Convert(value)
is not automatically better than:
Convert(value)
If Convert is a general-purpose operation and does not depend meaningfully on the receiver, a method may create unnecessary coupling.
Adding generic methods everywhere
Generics can reduce duplication, but excessive abstraction can make APIs harder to read.
A method should communicate meaningful ownership.
Good:
users.Map(...)
Potentially questionable:
users.Serialize(...)
users.Hash(...)
users.Validate(...)
users.Sort(...)
users.Normalize(...)
users.Compare(...)
Whether these belong on users depends on the application's domain and the responsibilities of the type.
Confusing receiver type parameters with method type parameters
Consider:
type Box[T any] struct {
Value T
}
func (b Box[T]) Convert[R any](fn func(T) R) R {
return fn(b.Value)
}
There are two independent generic parameters:
Box[T]
|
+-- T belongs to the type
Convert[R]
|
+-- R belongs to the method
Keeping that distinction clear is essential when designing more complex APIs.
Generic Methods vs Generic Functions: A Practical Decision
Use a generic method when:
The receiver is the natural owner of the operation.
The operation should be discoverable through the receiver.
Method chaining improves readability.
The method transforms or derives data from the receiver.
Keeping functionality attached to a type makes the package easier to organize.
Use a generic function when:
The operation is independent of a particular receiver.
Multiple unrelated types can use the same operation.
The function represents a general algorithm.
Interface-oriented design is central to the abstraction.
A method would exist mainly to avoid writing a package-level function.
The following mental model is useful:
Does the operation naturally belong to this value?
|
+----+----+
Yes No
| |
Method Function
|
Generic?
|
Yes
Best Practices for Production APIs
Keep the receiver meaningful
A generic method should use its receiver in a meaningful way.
Avoid methods where the receiver exists only to provide a namespace.
Prefer type inference
If Go can infer the method's type argument, let it.
Prefer:
result := values.Map(strconv.Itoa)
over unnecessarily verbose explicit type arguments.
Keep transformations predictable
Methods such as Map should have clear semantics.
If the method changes state, allocates new objects, performs I/O, or has other side effects, document those behaviors clearly.
Avoid generic methods for unrelated concerns
A type should not become a dumping ground for every operation that happens to involve it.
Good API design remains more important than using a new language feature.
Test multiple type combinations
Generic methods should be tested with representative type combinations.
For example:
func TestListMap(t *testing.T) {
values := List[int]{1, 2, 3}
result := values.Map(func(n int) string {
return strconv.Itoa(n)
})
want := List[string]{"1", "2", "3"}
if !reflect.DeepEqual(result, want) {
t.Fatalf("got %v, want %v", result, want)
}
}
For reusable libraries, test both common and boundary cases.
Troubleshooting Generic Method Errors
Error: method has type parameters
If the compiler rejects a generic method, first verify the project is actually building with Go 1.27 or newer.
Check:
go version
Also check the module configuration:
module example.com/myapp
go 1.27
Your development environment, CI environment, and production build environment should use compatible Go versions.
Error involving interfaces
If a generic method is being used as an implementation of an interface, review the interface definition.
A generic concrete method cannot satisfy an interface requirement that would require a generic interface method.
For example, this design is not supported:
type Transformer interface {
Transform[T any](T) T
}
Use a different abstraction, such as a non-generic interface or a generic function.
Type inference fails
If the compiler cannot determine the method's type argument, make it explicit:
result := values.Map[string](transform)
Explicit type arguments are useful when the result type cannot be inferred clearly from the arguments.
Advantages of Generic Methods
Better API organization
Behavior can remain attached to the type that owns it.
Cleaner method chains
Transformations can naturally flow from one result to another:
values.
Map(transform).
Filter(predicate).
Map(format)
Less package-level clutter
Related operations do not all have to become top-level functions.
Strong type safety
The compiler verifies relationships between the receiver, method parameters, and method result types.
Better reuse
A single method implementation can support many destination types.
Disadvantages and Limitations
Generic interface methods are still unavailable
This is the most important language limitation.
More complex APIs are possible
Generic methods add another dimension of type parameters to an already generic type.
Type[T].Method[R]()
This can become difficult to understand if used excessively.
Not every generic function should become a method
For general algorithms, package-level generic functions can remain clearer.
API design still matters
The availability of a language feature does not automatically mean an API should use it.
Conclusion
Go 1.27 makes generic methods a significant addition to the language's generics model.
The feature is especially valuable when a method needs to transform a receiver's type into another type:
type List[T any] []T
func (l List[T]) Map[R any](fn func(T) R) List[R] {
result := make(List[R], len(l))
for i, item := range l {
result[i] = fn(item)
}
return result
}
This design keeps the operation close to the type it belongs to and enables readable method chains.
However, generic methods are not a universal replacement for generic functions. They are concrete methods, not generic interface methods, and they cannot be used to introduce type parameters into interface method definitions.
The most practical approach is to treat generic methods as an API organization tool. Use them when behavior naturally belongs to a receiver, particularly for transformations and fluent operations. Continue using generic functions when an operation is independent of a specific type or represents a general-purpose algorithm.
Used selectively, generic methods make Go APIs more expressive without requiring developers to abandon the simplicity that has always been central to Go.

Join the conversation! Your thoughts help the community grow.