{"id":128,"date":"2026-04-20T13:02:11","date_gmt":"2026-04-20T13:02:11","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/golang-generics-three-patterns-i-actually-use\/"},"modified":"2026-04-20T13:02:11","modified_gmt":"2026-04-20T13:02:11","slug":"golang-generics-three-patterns-i-actually-use","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/golang-generics-three-patterns-i-actually-use\/","title":{"rendered":"Go generics: the three patterns I actually use"},"content":{"rendered":"<p>Confession: when Go 1.18 shipped generics in early 2022, I read the release notes, wrote a toy <code>Map[T, U]<\/code> helper, and then went right back to <code>interface{}<\/code> for another year. I wasn&rsquo;t being stubborn. I just couldn&rsquo;t point at anything in my day job where generics would make the code clearly better.<\/p>\n<p>Four years later my position is different, but not because I suddenly fell in love with type parameters. I found three specific patterns where Go generics actually pay their rent, and about five patterns where they quietly make my code worse. This post is the honest list.<\/p>\n<p>If you&rsquo;ve been reading Go posts where someone wraps every helper in a <code>[T any]<\/code> and calls it progress, this isn&rsquo;t one of those. I&rsquo;ll show the patterns I actually ship, with the old <code>interface{}<\/code> version next to the new one so you can see what changes and what doesn&rsquo;t.<\/p>\n<h2 id=\"pattern-1-typed-containers-the-features-clearest-win\">Pattern 1: typed containers, the feature&rsquo;s clearest win<\/h2>\n<p>Before 1.18, if I wanted a set in Go, I wrote a <code>map[string]struct{}<\/code> and moved on. If I needed one for ints too, I either duplicated the code or reached for <code>map[interface{}]struct{}<\/code> and learned to live with type assertions. Neither was great.<\/p>\n<p>Here&rsquo;s the old version:<\/p>\n<pre><code class=\"language-go\">\/\/ Pre-generics set, specialised for strings.\ntype StringSet map[string]struct{}\n\nfunc (s StringSet) Add(v string)      { s[v] = struct{}{} }\nfunc (s StringSet) Has(v string) bool { _, ok := s[v]; return ok }\n<\/code><\/pre>\n<p>And the generic one I actually use now:<\/p>\n<pre><code class=\"language-go\">type Set[T comparable] map[T]struct{}\n\nfunc NewSet[T comparable](xs ...T) Set[T] {\n    s := make(Set[T], len(xs))\n    for _, x := range xs {\n        s[x] = struct{}{}\n    }\n    return s\n}\n\nfunc (s Set[T]) Add(v T)      { s[v] = struct{}{} }\nfunc (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }\n<\/code><\/pre>\n<p>Three things I care about here. <code>comparable<\/code> is the right constraint for a map key and the compiler enforces it. There&rsquo;s zero boxing or type assertion at the call site. And I write this type once and it works for my string IDs, int64 primary keys, and custom struct keys. The readability cost is tiny because the set pattern is already familiar.<\/p>\n<p>I use the same shape for typed priority queues and bounded ring buffers. Containers are the clearest win generics gave Go, and the standard library agrees: the <a href=\"https:\/\/pkg.go.dev\/slices\" rel=\"nofollow noopener\" target=\"_blank\"><code>slices<\/code><\/a> and <a href=\"https:\/\/pkg.go.dev\/maps\" rel=\"nofollow noopener\" target=\"_blank\"><code>maps<\/code><\/a> packages added in 1.21 are exactly this pattern at scale.<\/p>\n<h2 id=\"pattern-2-thin-data-helpers-not-clever-ones\">Pattern 2: thin data helpers, not clever ones<\/h2>\n<p>My second pattern sits one level up from containers: tiny generic helpers over slices that I keep reaching for.<\/p>\n<p>Before:<\/p>\n<pre><code class=\"language-go\">\/\/ I wrote something like this in every service.\nfunc FirstUser(users []User, match func(User) bool) (User, bool) {\n    for _, u := range users {\n        if match(u) {\n            return u, true\n        }\n    }\n    return User{}, false\n}\n<\/code><\/pre>\n<p>After:<\/p>\n<pre><code class=\"language-go\">func First[T any](xs []T, match func(T) bool) (T, bool) {\n    for _, x := range xs {\n        if match(x) {\n            return x, true\n        }\n    }\n    var zero T\n    return zero, false\n}\n<\/code><\/pre>\n<p>This looks small, but I&rsquo;d written maybe eight versions of it across services before 1.18. The generic <code>First<\/code> lives in one place, I import it from our internal <code>xslices<\/code> package, and every team uses the same signature. The trick is keeping these helpers thin: single purpose, no nested callbacks, no early-return cleverness. Once a helper grows past fifteen lines, I stop and ask whether it should be a method on a real type instead.<\/p>\n<p>For longer operations, the standard library&rsquo;s <a href=\"https:\/\/pkg.go.dev\/slices#IndexFunc\" rel=\"nofollow noopener\" target=\"_blank\"><code>slices.IndexFunc<\/code><\/a> and its neighbours cover most of what I used to write by hand. When Go 1.23 added <a href=\"https:\/\/go.dev\/blog\/range-functions\" rel=\"nofollow noopener\" target=\"_blank\">range-over-func iterators<\/a>, some of my homegrown helpers became one-liners, which was a genuinely satisfying diff.<\/p>\n<h2 id=\"pattern-3-functional-ish-adapters-at-service-boundaries\">Pattern 3: functional-ish adapters at service boundaries<\/h2>\n<p>This is the one that took me longest to trust.<\/p>\n<p>A typical service I work on fetches rows from Postgres, transforms them, and hands them to an HTTP handler. The transformation step used to be a tower of nested for-loops. It now looks like this:<\/p>\n<pre><code class=\"language-go\">type Mapper[In, Out any] func(In) Out\n\nfunc MapSlice[In, Out any](xs []In, f Mapper[In, Out]) []Out {\n    out := make([]Out, len(xs))\n    for i, x := range xs {\n        out[i] = f(x)\n    }\n    return out\n}\n<\/code><\/pre>\n<p>Used:<\/p>\n<pre><code class=\"language-go\">dtos := MapSlice(rows, func(r Row) RowDTO {\n    return RowDTO{ID: r.ID, Name: r.Name, Price: r.PriceCents \/ 100}\n})\n<\/code><\/pre>\n<p>I used to feel guilty about this, because Go&rsquo;s orthodoxy is &ldquo;just write the for-loop&rdquo;. The for-loop is fine. But when the transformer function is pure and obvious, <code>MapSlice<\/code> reads better than <code>out := make([]RowDTO, 0, len(rows))<\/code> followed by a loop, and it nudges me to write small testable <code>Mapper<\/code> functions instead of inlining mapping logic inside handlers.<\/p>\n<p>One rule I keep: no more than two type parameters per helper, and no constraints beyond <code>any<\/code>, <code>comparable<\/code>, or the built-in ordered types. The moment I want a method set in the constraint, I reach for a plain interface instead. Generics aren&rsquo;t the only tool in Go and they shouldn&rsquo;t pretend to be.<\/p>\n<h2 id=\"the-pattern-i-quietly-stopped-using\">The pattern I quietly stopped using<\/h2>\n<p>Here&rsquo;s the one I got wrong early: wrapping a helper in generics just in case I needed it later.<\/p>\n<pre><code class=\"language-go\">\/\/ Don't. This helper only ever takes user IDs.\nfunc LoadRecord[T any](ctx context.Context, id T) (*Record, error) {\n    row := db.QueryRow(ctx, &quot;select * from records where id = $1&quot;, id)\n    \/\/ ...\n}\n<\/code><\/pre>\n<p>If every call site passes a <code>string<\/code> user ID, the <code>[T any]<\/code> isn&rsquo;t earning its keep. It adds a tiny mental tax on every reader (&ldquo;what types does this accept? do I need a constraint?&rdquo;) without buying anything. I&rsquo;ve since ripped this kind of thing out of three services and the code got shorter and clearer.<\/p>\n<p>The fix is simple: keep the helper concrete until a second caller with a different type forces your hand. People quote the rule of three for refactoring; for generics I&rsquo;d lower it to two. One specialised function is not a problem. Two is a gentle nudge to think about whether a type parameter fits.<\/p>\n<h2 id=\"constraints-i-wish-id-learned-on-day-one\">Constraints I wish I&rsquo;d learned on day one<\/h2>\n<p>Generics in Go aren&rsquo;t only <code>[T any]<\/code>. Three constraints pulled their weight for me:<\/p>\n<ul>\n<li><code>comparable<\/code> for anything that goes in a map key or gets compared with <code>==<\/code>. The <code>Set<\/code> example above is the canonical use.<\/li>\n<li><code>cmp.Ordered<\/code> (added in 1.21) for anything you can use with <code>&lt;<\/code>, <code>&gt;<\/code>, <code>&lt;=<\/code>, <code>&gt;=<\/code>. Great for typed <code>Min<\/code>, <code>Max<\/code>, and simple sort helpers.<\/li>\n<li>Interface constraints with type sets, when you actually need a method. For example:<\/li>\n<\/ul>\n<pre><code class=\"language-go\">type Stringer interface{ String() string }\n\nfunc JoinStringers[T Stringer](xs []T, sep string) string {\n    parts := make([]string, len(xs))\n    for i, x := range xs {\n        parts[i] = x.String()\n    }\n    return strings.Join(parts, sep)\n}\n<\/code><\/pre>\n<p>The Go team&rsquo;s <a href=\"https:\/\/go.dev\/blog\/intro-generics\" rel=\"nofollow noopener\" target=\"_blank\">intro to generics<\/a> and the longer <a href=\"https:\/\/go.googlesource.com\/proposal\/+\/refs\/heads\/master\/design\/43651-type-parameters.md\" rel=\"nofollow noopener\" target=\"_blank\">design doc<\/a> spend a lot of pages on type sets, and it took me two reads before the syntax clicked. If you&rsquo;ve only ever used <code>[T any]<\/code>, spending an hour on type-set constraints is the best generic Go investment you can make this month.<\/p>\n<h2 id=\"when-i-still-reach-for-plain-interfaces\">When I still reach for plain interfaces<\/h2>\n<p>Not everything wants to be generic. I still use non-generic interfaces when:<\/p>\n<ul>\n<li>The call site holds a slice of differently-typed values that share behaviour, like <code>[]Notifier<\/code> where each <code>Notifier<\/code> is a different concrete type.<\/li>\n<li>I&rsquo;m writing a plugin boundary and I want runtime polymorphism, so adding a new implementation doesn&rsquo;t require the caller to recompile against a new type.<\/li>\n<li>The method set is genuinely all I care about and the concrete type is none of my business.<\/li>\n<\/ul>\n<p>Generics and interfaces don&rsquo;t compete. They solve different problems. Use generics when the caller picks the type. Use interfaces when the callee has to handle one of many types at runtime.<\/p>\n<p>If you want to see how these patterns shift when you change languages, I wrote up <a href=\"https:\/\/abrarqasim.com\/blog\/rust-vs-go-2026-what-i-reach-for\" rel=\"noopener\">Rust vs Go in 2026<\/a> recently, and Rust&rsquo;s trait\/generic model makes very different tradeoffs for similar problems. And if you&rsquo;re staring at a Go service that&rsquo;s grown a thicket of <code>[T any]<\/code> wrappers, <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my work<\/a> is public and the untangling-Go-services part is a thing I actually enjoy.<\/p>\n<h2 id=\"what-to-try-this-week\">What to try this week<\/h2>\n<p>Open your largest internal utility package. Find one function you&rsquo;ve copy-pasted with different types (the odds are high). Pick the most common version, add a single type parameter, and delete the duplicates. Run your tests. If they pass, ship it. If they don&rsquo;t, the failure will tell you which constraint the function actually needed.<\/p>\n<p>Start there. Don&rsquo;t rewrite your service. Don&rsquo;t rewrite your team&rsquo;s service either. Just replace one set of duplicated helpers with one generic one, live with it for a week, and see whether the next person who touches that code is happier or more confused. That&rsquo;s the only metric that matters.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Go generics shipped in 1.18 and I ignored them for a year. Here are the three patterns I now use in production, plus one I quietly stopped using.<\/p>\n","protected":false},"author":2,"featured_media":127,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"Go generics shipped in 1.18 and I ignored them for a year. Here are the three patterns I now use in production, plus one I quietly stopped using.","rank_math_focus_keyword":"golang generics","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[49,102,104,47,65,103],"class_list":["post-128","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-backend","tag-go-generics","tag-go-patterns","tag-golang","tag-programming-languages","tag-type-parameters"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/128","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=128"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/128\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/127"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=128"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=128"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=128"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}