Skip to content

Go 1.27 Generic Methods: The Helper Package I Finally Deleted

Go 1.27 Generic Methods: The Helper Package I Finally Deleted

Okay, this is going to sound dumb, but I have a package in a client’s Go codebase called listx whose entire reason for existing is that Go methods couldn’t have their own type parameters. Fourteen exported functions, every one of them a verb I wanted to hang off a type and couldn’t. listx.Map, listx.Filter, listx.GroupBy, listx.Uniq. Every caller imports it, every caller has to remember the argument order, and every code review has had at least one “why isn’t this a method” comment from someone new.

Go 1.27 shipped in August with generic methods, and last weekend I deleted that package. This post is about what changed in the language, what the rule is that still says no, and the two places where I reached for the new feature and then backed out.

What the old rule was and why it existed

Since generics landed in Go 1.18 you could write a generic type and a generic function, but a method could only use the type parameters its receiver already had. You could write func (l List[E]) Len() int because E came from List. You couldn’t write func (l List[E]) Map[F any](f func(E) F) List[F] because F was new, and methods weren’t allowed to introduce new type parameters.

The reason was interfaces. If a method could be generic, then a type with Map[F any] might or might not satisfy an interface that asked for Map(func(int) string) List[string], and the compiler would have to decide that at every conversion site with type arguments it didn’t have. The Go team wrote that up years ago in the generics FAQ and the answer stayed “not yet” through nine releases.

So we all wrote package-level functions instead. Here’s the shape of the thing I’ve been shipping since 2022, straight out of that listx package:

package listx

type List[E any] []E

func Map[E, F any](l List[E], f func(E) F) List[F] {
    out := make(List[F], 0, len(l))
    for _, x := range l {
        out = append(out, f(x))
    }
    return out
}

func Filter[E any](l List[E], keep func(E) bool) List[E] {
    out := l[:0:0]
    for _, x := range l {
        if keep(x) {
            out = append(out, x)
        }
    }
    return out
}

And here’s what a caller looks like. It works. It’s also backwards, which is the whole complaint.

ids := listx.Map(
    listx.Filter(orders, func(o Order) bool { return o.Paid }),
    func(o Order) string { return o.ID },
)

Read that aloud. You filter, then you map, but the code says map first. Every time.

What Go 1.27 lets you write instead

The spec change is one production rule: MethodDecl now has an optional TypeParameters clause after the method name. The example in the spec is almost exactly the function I’d been writing at package level:

type List[E any] []E

// Apply returns the list obtained from applying f to each element of l.
func (l List[E]) Apply[F any](f func(E) F) List[F] {
    r := make(List[F], len(l))
    for i, x := range l {
        r[i] = f(x)
    }
    return r
}

So listx.Map became a method, listx.Filter became a method (that one never needed a new type parameter, it was in the package for consistency), and the call site reads in the order things happen:

ids := orders.
    Filter(func(o Order) bool { return o.Paid }).
    Map(func(o Order) string { return o.ID })

Type inference works the way it does for generic functions. I never wrote Map[string] anywhere; the compiler took F from the closure’s return type. The spec is explicit that a generic method has to be instantiated before it’s called or used as a value, and in practice inference does that for you at every call site I converted. Fourteen functions, zero explicit type arguments.

The standard library got one of these too. math/rand/v2 now has a generic method (*Rand) N[Int intType](Int) Int alongside the top-level rand.N function that has been there since 1.22. Small thing, but it’s the canonical example of what the feature is for: a function that belonged on a type and had to live in the package namespace until now.

The rule that still says no

Here’s the part I got wrong for about a day.

Interfaces cannot declare generic methods, and a generic method cannot be used to satisfy an interface method. The release notes say it in one sentence and I read past it. I had an interface in the same codebase:

type Collection[E any] interface {
    Filter(func(E) bool) Collection[E]
    Len() int
}

and I assumed I could add Map[F any](func(E) F) Collection[F] to it now. I can’t. The interface satisfaction problem from the FAQ didn’t go away; the Go team drew the line at concrete types. A concrete List[E] can have a generic Map. An interface describing “things with a Map” still can’t, because there’s no finite set of method signatures to check against.

What that means in practice: generic methods are for concrete types you own. If your code is built around interfaces (repository interfaces, anything you mock in tests), the new feature won’t touch that layer. You still write the generic function that takes the interface as its first argument. I have four of those left in the codebase and they’re staying.

I’m not sure I’d have designed it any other way, but I do wish the release notes had led with the restriction instead of putting it in the last clause of the paragraph. It’s the thing that decides whether the feature applies to your code at all.

The struct literal change nobody is talking about

The same release changed composite literals in a way that’s easy to miss. A key in a struct literal can now be any valid field selector, not only a top-level field name. So if you have an embedded struct, you can set its fields inline:

type Base struct{ ID, Version int }
type Order struct {
    Base
    Total int
}

// Go 1.26 and earlier
o := Order{Base: Base{ID: 7, Version: 2}, Total: 900}

// Go 1.27
o := Order{ID: 7, Version: 2, Total: 900}

I have a lot of Base-style embedded structs, and a lot of test fixtures that repeat Base: Base{...} a hundred times. gofmt won’t rewrite these for you and neither will the new go fix modernizers as far as I can tell, so I did it with a regex and a nervous go test ./.... It’s a small cleanup. It also made one bug visible: a fixture that set Base.ID and then, four lines later, overwrote the whole Base. With the flattened form the duplicate key is a compile error, because the spec says a key can’t name a promoted field when the embedded struct is also set by another key.

One restriction to know before you regex your fixtures: the embedded fields you walk through to reach the promoted one can’t be pointers. type Order struct { *Base; Total int } doesn’t get the shorthand, and the error message the compiler gives you is accurate but terse. I had three of those, and for them the old Base: &Base{...} form stays.

Where I used it and then took it back out

Two places.

The first was a Result[T] type I use for pipeline stages, with a package function Then[T, U any](r Result[T], f func(T) (U, error)) Result[U]. I turned it into a method, the call sites got prettier, and then I looked at the stack traces from a failing stage. The method version inlined differently and the closure names in the trace changed. The release notes warn that the compiler now picks simpler names for function literals, and combined with the method conversion I lost the ability to tell two stages apart in pprof. I could have fixed that with named functions instead of closures. I put the package function back instead, because the trace readability mattered more than the call-site syntax for that type. That’s a judgment call and I might reverse it in six months.

The second was a generic Cache[K comparable, V any] where I wanted func (c *Cache[K, V]) GetAs[T any](k K) (T, bool) to do a type assertion on the way out. It compiles. It’s also a bad idea, because the type assertion fails at runtime and the generic signature makes it look like the compiler is checking something it isn’t. A method named GetAs[T] reads as “this is type safe.” It’s not. The old func GetAs[T any](c *Cache[K, any], k K) at least looked like the hack it was. I kept the hack looking like a hack.

What I’d do this week

If you have a helpers package like my listx, the migration is mechanical and worth an afternoon. Bump go.mod to go 1.27, move each Func[T, U](recv, args) to func (recv) Func[U](args), and let the compiler find every call site. Type inference handled all of mine.

Before you touch anything, run go vet on the current tree, because Go 1.27’s go test now runs the stdversion check by default and it will flag any standard library symbol newer than your go.mod directive. On the client codebase that turned up two uses of slices.Chunk in a module still declaring go 1.22. Nothing to do with generic methods, but you’ll hit it the moment you upgrade the toolchain, so hit it on purpose first.

While you’re in there, check whether any of the package functions you’re converting are used as values. slices.SortFunc(xs, listx.ByID) style code, or a function stored in a map. A method value works there too (orders.Map is a valid expression once instantiated), but the receiver gets bound at the point you take the value, which is a different lifetime than a package function has. I had one of these, a registry of transforms keyed by name, and it needed a one-line wrapper. Not hard, but it’s the one place where the fix wasn’t a search and replace, so it’s the place to look at by hand.

And don’t touch your interfaces. Read the one sentence in the release notes about them twice, then leave them alone.

I wrote about where I actually use type parameters in Go generics in 2026 back when methods couldn’t have them; most of that post still holds, and the parts that don’t are the parts I’ve covered here. The Go work I take on for clients is described at abrarqasim.com, if you have a listx of your own and would rather someone else spent the afternoon.