{"id":486,"date":"2026-07-21T05:00:53","date_gmt":"2026-07-21T05:00:53","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/go-generics-2026-where-i-actually-use-type-parameters\/"},"modified":"2026-07-21T05:00:53","modified_gmt":"2026-07-21T05:00:53","slug":"go-generics-2026-where-i-actually-use-type-parameters","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/go-generics-2026-where-i-actually-use-type-parameters\/","title":{"rendered":"Go Generics: Where I Actually Use Type Parameters"},"content":{"rendered":"<p>I lost an afternoon last month to a function called <code>mapStrings<\/code>. Then I found <code>mapInts<\/code> sitting two packages over. Then <code>mapUsers<\/code>, which was the same function a third time, with different variable names and an off-by-one in the capacity hint. All three were mine. I have no memory of writing any of them.<\/p>\n<p>Go has shipped generics since 1.18, which means I had type parameters available the entire time I was copy-pasting that function around my own codebase. So this is partly an explainer and partly a confession.<\/p>\n<p>Short version if you&rsquo;re in a hurry: type parameters earn their keep in containers and small utility functions. They usually don&rsquo;t earn it in business logic. The line between those two is fuzzier than I expected, and I&rsquo;ve now crossed it in both directions and had to walk back.<\/p>\n<h2 id=\"the-helper-i-wrote-four-times\">The helper I wrote four times<\/h2>\n<p>Here&rsquo;s 2021 me, pre-generics, doing the obvious thing:<\/p>\n<pre><code class=\"language-go\">func MapStrings(in []string, f func(string) string) []string {\n    out := make([]string, 0, len(in))\n    for _, v := range in {\n        out = append(out, f(v))\n    }\n    return out\n}\n\nfunc MapInts(in []int, f func(int) int) []int {\n    out := make([]int, 0, len(in))\n    for _, v := range in {\n        out = append(out, f(v))\n    }\n    return out\n}\n<\/code><\/pre>\n<p>I did try the <code>interface{}<\/code> route once, on the theory that one function beats four. It was worse in every way that mattered. Every call site needed a type assertion, the compiler stopped helping me, and a mistake that used to be a build failure turned into a panic in staging at an hour I&rsquo;d rather not discuss.<\/p>\n<pre><code class=\"language-go\">func Map(in []interface{}, f func(interface{}) interface{}) []interface{} {\n    \/\/ one function, and now every caller pays for it in assertions\n}\n<\/code><\/pre>\n<p>The generic version is what I should have written:<\/p>\n<pre><code class=\"language-go\">func Map[T, U any](in []T, f func(T) U) []U {\n    out := make([]U, 0, len(in))\n    for _, v := range in {\n        out = append(out, f(v))\n    }\n    return out\n}\n\nnames := Map(users, func(u User) string { return u.Name })\n<\/code><\/pre>\n<p>Note the call site. You rarely write <code>Map[User, string]<\/code> by hand because inference handles it from the arguments. That inference is the reason this feature is pleasant rather than noisy, and it&rsquo;s the thing that makes generic Go read like normal Go. The <a href=\"https:\/\/go.dev\/blog\/intro-generics\" rel=\"nofollow noopener\" target=\"_blank\">generics tutorial on go.dev<\/a> covers the same shape if you want the language team&rsquo;s framing.<\/p>\n<h2 id=\"constraints-are-where-the-actual-thinking-happens\">Constraints are where the actual thinking happens<\/h2>\n<p><code>any<\/code> and <code>comparable<\/code> cover most of what I write. <code>comparable<\/code> is the one people trip on: it means the type supports <code>==<\/code>, so slices, maps and functions are out. If you&rsquo;re building a generic set or using a type parameter as a map key, <code>comparable<\/code> is your constraint.<\/p>\n<p>Union constraints are where it gets more interesting:<\/p>\n<pre><code class=\"language-go\">type Number interface {\n    ~int | ~int64 | ~float64\n}\n\nfunc Sum[T Number](xs []T) T {\n    var total T\n    for _, x := range xs {\n        total += x\n    }\n    return total\n}\n<\/code><\/pre>\n<p>That tilde matters, and I got it wrong for a solid week. Plain <code>int<\/code> in a constraint means exactly <code>int<\/code>. <code>~int<\/code> means any type whose underlying type is <code>int<\/code>, which includes your <code>type UserID int<\/code>. Without the tilde, <code>Sum([]UserID{...})<\/code> doesn&rsquo;t compile, and the error points at the constraint rather than at the missing squiggle, so you go looking in the wrong place. The <a href=\"https:\/\/go.dev\/ref\/spec#Type_parameter_declarations\" rel=\"nofollow noopener\" target=\"_blank\">spec section on type parameter declarations<\/a> is dry reading but it&rsquo;s the only place that states this precisely.<\/p>\n<p>Constraints can also require methods, which is how you write something generic over anything with a <code>String()<\/code> on it. In practice I reach for that less than I expected. If all I need is a method, a plain interface parameter already worked fine and doesn&rsquo;t need type parameters at all.<\/p>\n<h2 id=\"inference-stops-at-the-arguments\">Inference stops at the arguments<\/h2>\n<p>This one cost me an evening. Inference works from what you pass in, not from what you assign the result to.<\/p>\n<pre><code class=\"language-go\">func Keys[K comparable, V any](m map[K]V) []K {\n    out := make([]K, 0, len(m))\n    for k := range m {\n        out = append(out, k)\n    }\n    return out\n}\n\nids := Keys(usersByID) \/\/ fine, K and V come from the argument\n<\/code><\/pre>\n<p>But a function shaped like a constructor has nothing to infer from:<\/p>\n<pre><code class=\"language-go\">func Zero[T any]() T {\n    var z T\n    return z\n}\n\nn := Zero[int]() \/\/ explicit instantiation required\n<\/code><\/pre>\n<p>Once that clicked, most of my confusion about &ldquo;why does this one need brackets&rdquo; went away. If the type parameter doesn&rsquo;t appear in a parameter, you&rsquo;re typing it out.<\/p>\n<h2 id=\"where-i-backed-generics-out\">Where I backed generics out<\/h2>\n<p>I built a <code>Repository[T]<\/code> once. Generic <code>Get<\/code>, <code>List<\/code>, <code>Save<\/code>, the lot.<\/p>\n<p>It looked clean for about three weeks. Then the user repo needed a lookup by email, the invoice repo needed a date range filter with pagination, and the audit repo needed to never delete anything. I ended up with a generic base plus a pile of per-type methods hanging off it, which came to more code than the three concrete structs I started with, and with an extra layer of indirection to read through.<\/p>\n<p>Generics compress code that was genuinely identical and only differed by type. <code>Map<\/code> was identical. My repositories only looked identical from a distance, and the distance was doing all the work.<\/p>\n<p>There&rsquo;s a hard constraint that pushes the same direction: you can&rsquo;t add type parameters to methods, only to functions and types. If your design wants <code>func (r *Repo) Find[T any](...)<\/code>, Go says no. Better to find that out before you draw the diagram.<\/p>\n<h2 id=\"the-standard-library-quietly-deleted-half-my-utility-package\">The standard library quietly deleted half my utility package<\/h2>\n<p>This is the part I underused for far too long. <code>slices<\/code> and <code>maps<\/code> are in the standard library now, both built on type parameters, and between them they cover a lot of what lives in everyone&rsquo;s <code>internal\/util<\/code>.<\/p>\n<p>Before:<\/p>\n<pre><code class=\"language-go\">sort.Slice(users, func(i, j int) bool {\n    return users[i].Age &lt; users[j].Age\n})\n\nfound := false\nfor _, u := range users {\n    if u.ID == target {\n        found = true\n        break\n    }\n}\n<\/code><\/pre>\n<p>After:<\/p>\n<pre><code class=\"language-go\">slices.SortFunc(users, func(a, b User) int {\n    return cmp.Compare(a.Age, b.Age)\n})\n\nfound := slices.ContainsFunc(users, func(u User) bool { return u.ID == target })\n<\/code><\/pre>\n<p><code>slices.SortFunc<\/code> wants a comparison that returns an int, not a bool, which caught me out the first time I swapped one in. Read <a href=\"https:\/\/pkg.go.dev\/slices\" rel=\"nofollow noopener\" target=\"_blank\">the slices package docs<\/a> before you write anything that loops over a slice looking for something. There&rsquo;s a good chance it already exists and has been fuzzed harder than your version.<\/p>\n<p>Generic iterators are the other half of this story and they changed how I write pipelines enough that I gave them their own writeup on <a href=\"https:\/\/abrarqasim.com\/blog\/go-1-23-iterators-hand-rolled-boilerplate-i-stopped-writing\" rel=\"noopener\">Go 1.23 iterators<\/a>.<\/p>\n<h2 id=\"the-rough-edges-ive-actually-hit\">The rough edges I&rsquo;ve actually hit<\/h2>\n<p>Cold build times went up on the packages where I lean hardest on type parameters. Not dramatically, but enough to notice on a fresh checkout.<\/p>\n<p>Constraint error messages are still rougher than the rest of Go&rsquo;s diagnostics. When inference fails you get told a constraint isn&rsquo;t satisfied, and working out which of your type parameters caused it is on you.<\/p>\n<p>And signatures get heavy fast. <code>func Reduce[T, U any](in []T, init U, f func(U, T) U) U<\/code> is readable on its own. Three of those stacked in one file is a wall.<\/p>\n<h2 id=\"what-id-actually-do-this-week\">What I&rsquo;d actually do this week<\/h2>\n<p>Grep your codebase for functions whose names differ only by a type: <code>mapStrings<\/code> and <code>mapInts<\/code>, <code>findUser<\/code> and <code>findInvoice<\/code>. If the bodies are the same once you squint past the types, that&rsquo;s exactly the pile generics were built for. Everything else, leave alone.<\/p>\n<p>Then read the <code>slices<\/code> and <code>maps<\/code> docs properly and delete whatever they already do for you. I got about 120 lines back that way, which is the kind of quiet cleanup that never makes a changelog but makes the next person&rsquo;s week easier. It&rsquo;s the sort of thing I end up doing on most backends I take on, and there&rsquo;s more of that in <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">my work<\/a>. Concurrency helpers are the other place worth a sweep, which I went into separately in my notes on <a href=\"https:\/\/abrarqasim.com\/blog\/golang-concurrency-patterns-2026-errgroup-context-i-actually-ship\" rel=\"noopener\">errgroup and context patterns<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I wrote the same map helper four times before using Go generics. Where type parameters earn their keep, where I backed them out, and what slices replaced.<\/p>\n","protected":false},"author":2,"featured_media":485,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I wrote the same map helper four times before using Go generics. Where type parameters earn their keep, where I backed them out, and what slices replaced.","rank_math_focus_keyword":"golang generics","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[49,285,46,47,103],"class_list":["post-486","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-backend","tag-generics","tag-go","tag-golang","tag-type-parameters"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/486","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=486"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/486\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/485"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=486"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=486"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=486"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}