{"id":681,"date":"2026-09-14T05:04:54","date_gmt":"2026-09-14T05:04:54","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/go-1-27-generic-methods-the-listx-package-i-finally-deleted\/"},"modified":"2026-09-14T05:04:54","modified_gmt":"2026-09-14T05:04:54","slug":"go-1-27-generic-methods-the-listx-package-i-finally-deleted","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/go-1-27-generic-methods-the-listx-package-i-finally-deleted\/","title":{"rendered":"Go 1.27 Generic Methods: The Helper Package I Finally Deleted"},"content":{"rendered":"<p>Okay, this is going to sound dumb, but I have a package in a client&rsquo;s Go codebase called <code>listx<\/code> whose entire reason for existing is that Go methods couldn&rsquo;t have their own type parameters. Fourteen exported functions, every one of them a verb I wanted to hang off a type and couldn&rsquo;t. <code>listx.Map<\/code>, <code>listx.Filter<\/code>, <code>listx.GroupBy<\/code>, <code>listx.Uniq<\/code>. Every caller imports it, every caller has to remember the argument order, and every code review has had at least one &ldquo;why isn&rsquo;t this a method&rdquo; comment from someone new.<\/p>\n<p>Go 1.27 shipped in August with <a href=\"https:\/\/go.dev\/doc\/go1.27\" rel=\"nofollow noopener\" target=\"_blank\">generic methods<\/a>, 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.<\/p>\n<h2 id=\"what-the-old-rule-was-and-why-it-existed\">What the old rule was and why it existed<\/h2>\n<p>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 <code>func (l List[E]) Len() int<\/code> because <code>E<\/code> came from <code>List<\/code>. You couldn&rsquo;t write <code>func (l List[E]) Map[F any](f func(E) F) List[F]<\/code> because <code>F<\/code> was new, and methods weren&rsquo;t allowed to introduce new type parameters.<\/p>\n<p>The reason was interfaces. If a method could be generic, then a type with <code>Map[F any]<\/code> might or might not satisfy an interface that asked for <code>Map(func(int) string) List[string]<\/code>, and the compiler would have to decide that at every conversion site with type arguments it didn&rsquo;t have. The Go team wrote that up years ago in the generics FAQ and the answer stayed &ldquo;not yet&rdquo; through nine releases.<\/p>\n<p>So we all wrote package-level functions instead. Here&rsquo;s the shape of the thing I&rsquo;ve been shipping since 2022, straight out of that <code>listx<\/code> package:<\/p>\n<pre><code class=\"language-go\">package listx\n\ntype List[E any] []E\n\nfunc Map[E, F any](l List[E], f func(E) F) List[F] {\n    out := make(List[F], 0, len(l))\n    for _, x := range l {\n        out = append(out, f(x))\n    }\n    return out\n}\n\nfunc Filter[E any](l List[E], keep func(E) bool) List[E] {\n    out := l[:0:0]\n    for _, x := range l {\n        if keep(x) {\n            out = append(out, x)\n        }\n    }\n    return out\n}\n<\/code><\/pre>\n<p>And here&rsquo;s what a caller looks like. It works. It&rsquo;s also backwards, which is the whole complaint.<\/p>\n<pre><code class=\"language-go\">ids := listx.Map(\n    listx.Filter(orders, func(o Order) bool { return o.Paid }),\n    func(o Order) string { return o.ID },\n)\n<\/code><\/pre>\n<p>Read that aloud. You filter, then you map, but the code says map first. Every time.<\/p>\n<h2 id=\"what-go-127-lets-you-write-instead\">What Go 1.27 lets you write instead<\/h2>\n<p>The <a href=\"https:\/\/go.dev\/ref\/spec#Method_declarations\" rel=\"nofollow noopener\" target=\"_blank\">spec change<\/a> is one production rule: <code>MethodDecl<\/code> now has an optional <code>TypeParameters<\/code> clause after the method name. The example in the spec is almost exactly the function I&rsquo;d been writing at package level:<\/p>\n<pre><code class=\"language-go\">type List[E any] []E\n\n\/\/ Apply returns the list obtained from applying f to each element of l.\nfunc (l List[E]) Apply[F any](f func(E) F) List[F] {\n    r := make(List[F], len(l))\n    for i, x := range l {\n        r[i] = f(x)\n    }\n    return r\n}\n<\/code><\/pre>\n<p>So <code>listx.Map<\/code> became a method, <code>listx.Filter<\/code> 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:<\/p>\n<pre><code class=\"language-go\">ids := orders.\n    Filter(func(o Order) bool { return o.Paid }).\n    Map(func(o Order) string { return o.ID })\n<\/code><\/pre>\n<p>Type inference works the way it does for generic functions. I never wrote <code>Map[string]<\/code> anywhere; the compiler took <code>F<\/code> from the closure&rsquo;s return type. The spec is explicit that a generic method has to be instantiated before it&rsquo;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.<\/p>\n<p>The standard library got one of these too. <code>math\/rand\/v2<\/code> now has a generic method <code>(*Rand) N[Int intType](Int) Int<\/code> alongside the top-level <code>rand.N<\/code> function that has been there since 1.22. Small thing, but it&rsquo;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.<\/p>\n<h2 id=\"the-rule-that-still-says-no\">The rule that still says no<\/h2>\n<p>Here&rsquo;s the part I got wrong for about a day.<\/p>\n<p>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:<\/p>\n<pre><code class=\"language-go\">type Collection[E any] interface {\n    Filter(func(E) bool) Collection[E]\n    Len() int\n}\n<\/code><\/pre>\n<p>and I assumed I could add <code>Map[F any](func(E) F) Collection[F]<\/code> to it now. I can&rsquo;t. The interface satisfaction problem from the FAQ didn&rsquo;t go away; the Go team drew the line at concrete types. A concrete <code>List[E]<\/code> can have a generic <code>Map<\/code>. An interface describing &ldquo;things with a <code>Map<\/code>&rdquo; still can&rsquo;t, because there&rsquo;s no finite set of method signatures to check against.<\/p>\n<p>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&rsquo;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&rsquo;re staying.<\/p>\n<p>I&rsquo;m not sure I&rsquo;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&rsquo;s the thing that decides whether the feature applies to your code at all.<\/p>\n<h2 id=\"the-struct-literal-change-nobody-is-talking-about\">The struct literal change nobody is talking about<\/h2>\n<p>The same release changed composite literals in a way that&rsquo;s easy to miss. A <a href=\"https:\/\/go.dev\/ref\/spec#Composite_literals\" rel=\"nofollow noopener\" target=\"_blank\">key in a struct literal<\/a> 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:<\/p>\n<pre><code class=\"language-go\">type Base struct{ ID, Version int }\ntype Order struct {\n    Base\n    Total int\n}\n\n\/\/ Go 1.26 and earlier\no := Order{Base: Base{ID: 7, Version: 2}, Total: 900}\n\n\/\/ Go 1.27\no := Order{ID: 7, Version: 2, Total: 900}\n<\/code><\/pre>\n<p>I have a lot of <code>Base<\/code>-style embedded structs, and a lot of test fixtures that repeat <code>Base: Base{...}<\/code> a hundred times. <code>gofmt<\/code> won&rsquo;t rewrite these for you and neither will the new <code>go fix<\/code> modernizers as far as I can tell, so I did it with a regex and a nervous <code>go test .\/...<\/code>. It&rsquo;s a small cleanup. It also made one bug visible: a fixture that set <code>Base.ID<\/code> and then, four lines later, overwrote the whole <code>Base<\/code>. With the flattened form the duplicate key is a compile error, because the spec says a key can&rsquo;t name a promoted field when the embedded struct is also set by another key.<\/p>\n<p>One restriction to know before you regex your fixtures: the embedded fields you walk through to reach the promoted one can&rsquo;t be pointers. <code>type Order struct { *Base; Total int }<\/code> doesn&rsquo;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 <code>Base: &amp;Base{...}<\/code> form stays.<\/p>\n<h2 id=\"where-i-used-it-and-then-took-it-back-out\">Where I used it and then took it back out<\/h2>\n<p>Two places.<\/p>\n<p>The first was a <code>Result[T]<\/code> type I use for pipeline stages, with a package function <code>Then[T, U any](r Result[T], f func(T) (U, error)) Result[U]<\/code>. 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 <code>pprof<\/code>. 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&rsquo;s a judgment call and I might reverse it in six months.<\/p>\n<p>The second was a generic <code>Cache[K comparable, V any]<\/code> where I wanted <code>func (c *Cache[K, V]) GetAs[T any](k K) (T, bool)<\/code> to do a type assertion on the way out. It compiles. It&rsquo;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&rsquo;t. A method named <code>GetAs[T]<\/code> reads as &ldquo;this is type safe.&rdquo; It&rsquo;s not. The old <code>func GetAs[T any](c *Cache[K, any], k K)<\/code> at least looked like the hack it was. I kept the hack looking like a hack.<\/p>\n<h2 id=\"what-id-do-this-week\">What I&rsquo;d do this week<\/h2>\n<p>If you have a helpers package like my <code>listx<\/code>, the migration is mechanical and worth an afternoon. Bump <code>go.mod<\/code> to <code>go 1.27<\/code>, move each <code>Func[T, U](recv, args)<\/code> to <code>func (recv) Func[U](args)<\/code>, and let the compiler find every call site. Type inference handled all of mine.<\/p>\n<p>Before you touch anything, run <code>go vet<\/code> on the current tree, because Go 1.27&rsquo;s <code>go test<\/code> now runs the <code>stdversion<\/code> check by default and it will flag any standard library symbol newer than your <code>go.mod<\/code> directive. On the client codebase that turned up two uses of <code>slices.Chunk<\/code> in a module still declaring <code>go 1.22<\/code>. Nothing to do with generic methods, but you&rsquo;ll hit it the moment you upgrade the toolchain, so hit it on purpose first.<\/p>\n<p>While you&rsquo;re in there, check whether any of the package functions you&rsquo;re converting are used as values. <code>slices.SortFunc(xs, listx.ByID)<\/code> style code, or a function stored in a map. A method value works there too (<code>orders.Map<\/code> 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&rsquo;s the one place where the fix wasn&rsquo;t a search and replace, so it&rsquo;s the place to look at by hand.<\/p>\n<p>And don&rsquo;t touch your interfaces. Read the one sentence in the release notes about them twice, then leave them alone.<\/p>\n<p>I wrote about where I actually use type parameters in <a href=\"https:\/\/abrarqasim.com\/blog\/go-generics-2026-where-i-actually-use-type-parameters\/\" rel=\"noopener\">Go generics in 2026<\/a> back when methods couldn&rsquo;t have them; most of that post still holds, and the parts that don&rsquo;t are the parts I&rsquo;ve covered here. The Go work I take on for clients is described at <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">abrarqasim.com<\/a>, if you have a <code>listx<\/code> of your own and would rather someone else spent the afternoon.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Go 1.27 lets methods declare their own type parameters. I converted a 14-function helper package, hit the interface rule that still says no, and reverted two of them.<\/p>\n","protected":false},"author":2,"featured_media":680,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Go 1.27 lets methods declare their own type parameters. I converted a 14-function helper package, hit the interface rule that still says no, and reverted two of them.","rank_math_focus_keyword":"go 1.27","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[285,46,752,47,753,413],"class_list":["post-681","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-generics","tag-go","tag-go-1-27","tag-golang","tag-refactoring","tag-type-parameters-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/681","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=681"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/681\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/680"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=681"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=681"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=681"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}