Article
Go 1.27 Generic Methods: Finally Chain Methods Like in Java Streams
Go liked to keep its distance from Java’s more expressive style; fluent chains like .stream().filter().map().collect(), more like in functional programming, have not really been possible. That gap just got smaller. The recently released Go 1.27 adds generic methods, and it quietly unlocks this kind of chained, type-changing pipelines that Java developers can enjoy since Java 8.
The Change
To share some definition first to ensure a common understanding: In object-oriented programming, a function is a named stand-alone block of code, while a method is associated with a class/object/struct (Stackoverflow description).
Before Go 1.27, methods of a generic type were stuck using the same type parameters as the type itself. A method could never introduce a new one, only functions could do so. That meant that methods like Map, which want to turn a Container[T] into a Container[U], simply were not legal Go. As a workaround, you had to fall back to a free-standing function instead of a method.
Go 1.27 lifts that restriction: A method can now declare its own type parameters it wants to return, independent of the method’s receiver (the struct the method is called on).
1type Container[T any] struct{ items []T }
2
3// Generic U belongs to the method, not the receiver
4func (c Container[T]) Map[U any](f func(T) U) Container[U] {
5 out := make([]U, len(c.items))
6 for i, v := range c.items {
7 out[i] = f(v)
8 }
9 return Container[U]{items: out}
10}
According to the proposal, the language’s grammar does not need to be changed a lot, but it is the missing piece for properly fluent chaining.
What Java’s Stream API Does
Java’s Stream<T> lets you compose a chain of operations on a collection: filter elements, transform them (possibly into a different type), to then reduce or collect the result. All of this can happen in one readable chain:
1List<String> result = numbers.stream()
2 .filter(n -> n % 2 == 0)
3 .map(n -> "pre-" + n)
4 .toList();
The relevant detail is that .map() can change the parameter type in the mid of the collection’s stream; Stream<Integer> becomes Stream<String> and everything still reads as one fluent expression.
Same Shape in Go
With the new generic methods, Go can express the same pipeline with actual method chaining, instead of nesting function calls inside one another:
1result := NewContainer([]int{1, 2, 3, 4, 5}).
2 Filter(func(n int) bool { return n%2 == 0 }).
3 Map(func(n int) string { return fmt.Sprintf("pre-%d", n) })
Filter keeps the type T and Map swaps it out for a new U, inferred from the function you pass in. This is exactly like Stream<Integer>.map() becoming Stream<String>. It is however not a wholesale Java clone because Go has still no lazy evaluation built into the language so that there could be a significant performance benefit of streams. And importantly, interfaces can still not have generic methods. Anyway, for the common case of chaining filter/map/reduce over a typed collection, Go can finally be read the way Java has for over a decade.
Complex Example of Method Chaining in Go
To show how this helps in crafting readable code, look at the following example; it is possible to understand the domain problem without knowing the technical implementation details:
1orders := []Order{
2 {"A100", "Alice", "paid", 129.99},
3 {"A101", "Bob", "pending", 45.00},
4 {"A102", "Carol", "paid", 89.50},
5 {"A103", "Dave", "refunded", 220.00},
6 {"A104", "Eve", "paid", 310.25},
7}
8
9highValuePaid := Of(orders...).
10 Filter(isPaid).
11 Map(toInvoice).
12 Filter(isHighValue)
13
14fmt.Println("High-value paid invoices:")
15highValuePaid.ForEach(printInvoice)
16
17total := highValuePaid.Reduce(0.0, sumAmounts)
18fmt.Printf("Total due: $%.2f\n", total)