<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Prasad Ekke</title>
    <description>The latest articles on DEV Community by Prasad Ekke (@prasadekke).</description>
    <link>https://dev.to/prasadekke</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3974510%2Fc6f3041d-132c-4b94-8e70-cc3604077ef9.jpg</url>
      <title>DEV Community: Prasad Ekke</title>
      <link>https://dev.to/prasadekke</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prasadekke"/>
    <language>en</language>
    <item>
      <title>Determinism Is a Core Contract in Build Caching</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Tue, 04 Aug 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/prasadekke/determinism-is-a-core-contract-in-build-caching-449n</link>
      <guid>https://dev.to/prasadekke/determinism-is-a-core-contract-in-build-caching-449n</guid>
      <description>&lt;p&gt;If you have set up a remote build cache and watched the hit rate sit stubbornly low, nondeterministic outputs are one important suspect — but not the only one. Source churn, changing flags, platform differences, toolchain changes, eviction, and incomplete input declarations also cause misses. Nondeterminism is particularly dangerous because it can quietly reduce reuse or make cached results untrustworthy without producing an obvious build error. It is worth understanding exactly how that happens and how to isolate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why determinism is the contract, not a nice-to-have
&lt;/h2&gt;

&lt;p&gt;A build cache works by fingerprinting an action's inputs and using that fingerprint to look up a previously computed output. The entire scheme rests on one assumption: identical inputs produce identical outputs. That is the contract. Break it and one of two bad things happens.&lt;/p&gt;

&lt;p&gt;If the cache keys only on inputs — which is how these systems work — and your action is nondeterministic, then the cache will happily hand back &lt;em&gt;an&lt;/em&gt; output for those inputs, but not necessarily the one a fresh run would produce. Best case, the variation is cosmetic and harmless. Worst case, you have just served a subtly wrong artifact from cache and you will spend a very confusing afternoon. Either way, the cache is now a source of doubt rather than speed.&lt;/p&gt;

&lt;p&gt;Another operational outcome is cascading misses, and that is where determinism stops being abstract.&lt;/p&gt;

&lt;h2&gt;
  
  
  One bad action poisons everything downstream
&lt;/h2&gt;

&lt;p&gt;Builds are graphs. The output of one action is the input to the next. So consider what happens when a single early action — say, a code generator — embeds the current timestamp in its output. Each time that generator actually runs, the file it produces is byte-for-byte different, so its content hash changes.&lt;/p&gt;

&lt;p&gt;Whenever that generator has to execute again — because its entry was evicted, the cache was bypassed, or another declared input changed — it produces a new content digest. Every action that consumes the generated file then sees a different input and misses its previous cache entry. Their outputs feed further actions, so the effect can cascade through the subgraph. You did not merely lose reuse for one action; you invalidated downstream keys even when the semantic content did not need to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where nondeterminism actually comes from
&lt;/h2&gt;

&lt;p&gt;The sources are mundane, which is what makes them easy to miss. The usual suspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timestamps baked into outputs — &lt;code&gt;__DATE__&lt;/code&gt; and &lt;code&gt;__TIME__&lt;/code&gt; macros, build IDs, and the modification times stored inside archive formats like &lt;code&gt;.a&lt;/code&gt;, &lt;code&gt;.tar&lt;/code&gt;, and &lt;code&gt;.zip&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Absolute paths leaking into outputs — debug info that records the full build directory, &lt;code&gt;__FILE__&lt;/code&gt; expansions, paths embedded by the linker. Build the same code in &lt;code&gt;/home/alice&lt;/code&gt; and &lt;code&gt;/home/bob&lt;/code&gt; and you get different bytes.&lt;/li&gt;
&lt;li&gt;Iteration order — generating code by walking a hash map or set, whose iteration order is unspecified, so the output lines come out shuffled run to run.&lt;/li&gt;
&lt;li&gt;Concurrency — actions that assemble output from parallel work without imposing a stable order on the result.&lt;/li&gt;
&lt;li&gt;Randomness — UUIDs, unseeded PRNGs, anything that reaches for entropy at build time.&lt;/li&gt;
&lt;li&gt;The ambient environment — locale, hostname, username, and stray environment variables that the action reads but never declared as inputs.&lt;/li&gt;
&lt;li&gt;Filesystem ordering — relying on &lt;code&gt;readdir&lt;/code&gt; order, which varies across filesystems and runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these throws an error. Each just makes one action's output wiggle, and the cache does the rest of the damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to hunt them down
&lt;/h2&gt;

&lt;p&gt;The basic diagnostic is to execute the same build twice from clean, isolated directories with cache reuse disabled, then compare the outputs bit for bit. Anything that differs deserves investigation. &lt;a href="https://diffoscope.org/" rel="noopener noreferrer"&gt;&lt;code&gt;diffoscope&lt;/code&gt;&lt;/a&gt; is the right tool here — it understands archives, binaries, and debug information and can show not just &lt;em&gt;that&lt;/em&gt; two outputs differ but &lt;em&gt;where&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The fixes are well-trodden, because the reproducible-builds community has been fighting this war for years:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timestamps: honor &lt;code&gt;SOURCE_DATE_EPOCH&lt;/code&gt; so embedded dates are fixed rather than "now"; build archives in deterministic mode (&lt;code&gt;tar --sort=name --mtime=...&lt;/code&gt;, &lt;code&gt;ar&lt;/code&gt; in its deterministic &lt;code&gt;D&lt;/code&gt; mode).&lt;/li&gt;
&lt;li&gt;Absolute paths: use &lt;code&gt;-ffile-prefix-map&lt;/code&gt; / &lt;code&gt;-fdebug-prefix-map&lt;/code&gt; to rewrite build paths to a stable placeholder, so debug info does not depend on where you built.&lt;/li&gt;
&lt;li&gt;Iteration order: sort before you emit. If codegen walks a map, sort the keys first. Never let an unordered container decide output order.&lt;/li&gt;
&lt;li&gt;Environment: pin the locale (&lt;code&gt;LC_ALL=C&lt;/code&gt;), and run actions in a sandbox that only exposes declared inputs, so an undeclared environment dependency fails loudly instead of silently varying.&lt;/li&gt;
&lt;li&gt;Randomness: seed it, or design it out. A build is not the place for entropy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hermeticity is how you keep it fixed
&lt;/h2&gt;

&lt;p&gt;Fixing the determinism bugs you have is half the job. The other half is preventing new ones, and hermetic, sandboxed builds remove many sources of ambient input. A filesystem sandbox can hide undeclared files and tools, and a controlled environment can pin locale and environment variables. Sandboxing alone does not necessarily hide the wall clock, randomness, hostname, kernel details, or every network source; those must be blocked, virtualized, or deliberately normalized. The goal is to make undeclared dependencies fail loudly or become stable rather than silently changing outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure it, because it regresses
&lt;/h2&gt;

&lt;p&gt;Determinism is not a one-time cleanup. It rots. Someone adds a code generator, someone embeds a build timestamp "just for diagnostics," and reuse slides without anyone noticing. Treat cache hit rate as a first-class metric, but segment it by action type, platform, branch, and miss reason because determinism is only one contributor. Track reproducibility separately with scheduled rebuild-and-compare tests. When hit rate drops and action keys churn without meaningful input changes, reach for build-twice-and-diff before blaming the cache infrastructure.&lt;/p&gt;

&lt;p&gt;That is the real point. Determinism is not purity for its own sake. Along with complete input declarations, compatible platforms and toolchains, and retained cache entries, it is a precondition for trustworthy reuse. Get it wrong and the cache may return questionable outputs or lose reuse across a large subgraph. Get it right and equivalent actions can reliably share results across developers and CI. Determinism is not the whole cache system, but it is part of its correctness contract.&lt;/p&gt;

&lt;p&gt;Further reading: the &lt;a href="https://reproducible-builds.org/docs/" rel="noopener noreferrer"&gt;Reproducible Builds documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>build</category>
      <category>buildsystems</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Remote Execution, Explained for People Who Just Run `make`</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Tue, 28 Jul 2026 12:02:00 +0000</pubDate>
      <link>https://dev.to/prasadekke/remote-execution-explained-for-people-who-just-run-make-4lhi</link>
      <guid>https://dev.to/prasadekke/remote-execution-explained-for-people-who-just-run-make-4lhi</guid>
      <description>&lt;p&gt;Most engineers think of the build as something that happens on their laptop. You type a command, the fans spin up, and a few minutes later you have a binary. That model is so ingrained that we rarely question it — until the codebase gets big enough that it quietly collapses.&lt;/p&gt;

&lt;p&gt;At a certain scale, the laptop build is too big for one machine, too slow to repeat, and too wasteful to run from scratch on every change. Ten engineers rebuild the same unchanged files ten times before lunch. A clean build takes forty minutes. CI is the bottleneck for the whole team. Remote execution is the answer large codebases reach for, and once you understand it, a lot of otherwise-baffling build-system design suddenly makes sense. Here is the mental model, minus the vendor jargon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: the build is a graph of pure functions
&lt;/h2&gt;

&lt;p&gt;Stop thinking of a build as a sequence of commands and start thinking of it as a graph. Each node is an &lt;em&gt;action&lt;/em&gt;: a single command plus the exact set of inputs it reads and outputs it produces. Compiling one file is an action. Linking is an action. Generating code from a schema is an action. The edges are dependencies — the linker action consumes the object files the compile actions produced.&lt;/p&gt;

&lt;p&gt;Here is the key insight that everything else rests on. If you treat each action as a pure function — same inputs always produce the same outputs — then two things become possible that are not possible with a pile of shell scripts. You can run independent actions anywhere, on any machine, in parallel. And you can cache the result of any action, because identical inputs mean you already know the output.&lt;/p&gt;

&lt;p&gt;Remote execution is what you get when you take those two possibilities seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an action actually is
&lt;/h2&gt;

&lt;p&gt;Concretely, an action is described by its command, the content of every input it depends on, and the environment it runs in. Crucially, inputs are identified by the hash of their content, not their path or timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Action {
  command:       ["clang", "-c", "parser.c", "-o", "parser.o"]
  input_digests: { "parser.c": sha256(abc1...),
                   "parser.h": sha256(def2...) }
  platform:      { os: linux, arch: x86_64, ... }
}

action_digest = sha256(serialize(Action))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;action_digest&lt;/code&gt; is the core lookup key. It fingerprints everything the build system has declared as relevant to the output. Correctness therefore depends on complete input and environment declarations: an undeclared dependency is invisible to the key. The build system computes the digest and asks a shared service one question: have you seen this exact declared action before?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cache hit: the service returns the output blobs immediately. No compilation happens at all — not locally, not remotely. You just fetched the answer.&lt;/li&gt;
&lt;li&gt;Cache miss: the service schedules the action on a remote worker, the worker runs the command in a clean sandbox, uploads the resulting &lt;code&gt;parser.o&lt;/code&gt; to a content-addressed store, and records the action-to-output mapping so the next person gets a hit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multiply that across a team and a CI fleet and the effect is dramatic. The first engineer to build a given commit pays the cost; everyone else, and every CI run on that commit, gets cache hits. Work is done once and shared.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two pieces of machinery
&lt;/h2&gt;

&lt;p&gt;Underneath, remote execution rests on two services that are simpler than they sound.&lt;/p&gt;

&lt;p&gt;The first is a content-addressed store (the CAS). It is a blob store keyed by the hash of the content. Put bytes in, get a hash back; present a hash, get the bytes. Because the key &lt;em&gt;is&lt;/em&gt; the content's fingerprint, identical files can be deduplicated and a retained blob cannot become stale under the same digest. Storage policies may evict blobs, so clients still have to handle missing content. Source files, intermediate objects, and final binaries can all live here.&lt;/p&gt;

&lt;p&gt;The second is the action cache: a map from &lt;code&gt;action_digest&lt;/code&gt; to the set of output digests it produced. That is the lookup that turns "compile this" into "you already compiled this, here are the bytes."&lt;/p&gt;

&lt;p&gt;Bazel and Buck2 both support the common &lt;a href="https://github.com/bazelbuild/remote-apis" rel="noopener noreferrer"&gt;Remote Execution API&lt;/a&gt; for these services, which allows compatible deployments to point either client at the same class of remote backend. You do not need to know the protocol to use it, but it helps to know it exists: the build tool on your laptop coordinates the graph while remote workers and shared caches handle much of the expensive execution and storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this reshapes how build systems are designed
&lt;/h2&gt;

&lt;p&gt;Once you see the build as a graph of cacheable pure functions, the design decisions of modern build systems stop looking arbitrary.&lt;/p&gt;

&lt;p&gt;Why does Bazel make you declare every input and output explicitly, instead of letting a command read whatever files it wants? Because the action digest has to capture every input. An undeclared input is an input the cache cannot see, which means the cache will hand you a stale result. Why the obsession with hermetic, sandboxed actions? Because a worker on the other side of the cluster has to be able to run your action with nothing but the declared inputs and get the same answer your laptop would. The strictness that feels like bureaucracy when you are writing a &lt;code&gt;BUILD&lt;/code&gt; file is the exact thing that makes the action a pure function — and a pure function is the thing you can distribute and cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it gets hard
&lt;/h2&gt;

&lt;p&gt;It is not free, and the failure modes are worth knowing before you adopt it.&lt;/p&gt;

&lt;p&gt;The network becomes the new bottleneck. You have traded local CPU for cache lookups and blob transfers, so a slow link or a far-away CAS can make a "cached" build feel slower than a local one. Large inputs and outputs strain the CAS and the network; build graphs with giant artifacts need care. And the whole edifice rests on one assumption that deserves its own discussion: that actions are actually deterministic. The moment an action produces different output from identical inputs, the cache starts handing out wrong or useless answers, and the value proposition quietly inverts.&lt;/p&gt;

&lt;p&gt;That last point is important enough that it is the subject of its own post. For now, the mental model is the takeaway: a build is a graph of actions, each action is a pure function fingerprinted by its inputs, and remote execution is just the infrastructure for running those functions anywhere and remembering their answers. Once that clicks, the rest of the modern build world — the explicit inputs, the sandboxing, the content hashing — reads as the obvious consequence it is.&lt;/p&gt;

</description>
      <category>build</category>
      <category>buildsystems</category>
      <category>distributedsystems</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Go Slice Initialization Gotchas: The Hole-Creating Bug I Keep Seeing (And a Linter That Catches It)</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Sun, 26 Jul 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/prasadekke/go-slice-initialization-gotchas-the-hole-creating-bug-i-keep-seeing-and-a-linter-that-catches-it-2afl</link>
      <guid>https://dev.to/prasadekke/go-slice-initialization-gotchas-the-hole-creating-bug-i-keep-seeing-and-a-linter-that-catches-it-2afl</guid>
      <description>&lt;p&gt;Go slices are simple on the surface. Under the hood, the interaction between length, capacity, and &lt;code&gt;append&lt;/code&gt; has a specific failure mode I've been bitten by more than once — and seen in enough code reviews to write about. The bug is invisible at compile time, often invisible in tests, and can corrupt data silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three ways to initialize a slice
&lt;/h2&gt;

&lt;p&gt;Before the bug, the mechanics. &lt;code&gt;make&lt;/code&gt; for slices takes up to three arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These behave very differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Length 0, capacity 100&lt;/span&gt;
&lt;span class="c"&gt;// Content: []&lt;/span&gt;
&lt;span class="c"&gt;// append() fills from index 0&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Length 5, capacity 5&lt;/span&gt;
&lt;span class="c"&gt;// Content: [0 0 0 0 0]&lt;/span&gt;
&lt;span class="c"&gt;// Indexed assignment fills existing slots&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Length 5, capacity 10&lt;/span&gt;
&lt;span class="c"&gt;// Content: [0 0 0 0 0]&lt;/span&gt;
&lt;span class="c"&gt;// Indexed assignment fills existing slots 0–4&lt;/span&gt;
&lt;span class="c"&gt;// append() fills from index 5 onward&lt;/span&gt;
&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key rule: &lt;strong&gt;&lt;code&gt;append&lt;/code&gt; always starts after the last element, at index &lt;code&gt;len(slice)&lt;/code&gt;&lt;/strong&gt;. It does not fill from the beginning. It does not know or care what's in the existing slots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c"&gt;// [0 0 0 0 0]&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c"&gt;// 5&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c"&gt;// 10&lt;/span&gt;

&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c"&gt;// [0 0 0 0 0 42]  ← 42 is at index 5, not 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is correct, expected behavior. The problem arises when you mix the two initialization patterns during a refactor.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug: optimization that creates holes
&lt;/h2&gt;

&lt;p&gt;Here's the sequence of events. You have code that initializes a slice with a fixed length and fills it by index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Version 1: works correctly&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// results: [r0 r1 r2 r3 r4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fine. Five jobs, five slots, indexed assignment, everything lines up.&lt;/p&gt;

&lt;p&gt;Later, someone (possibly future-you) decides to optimize memory allocation by pre-allocating capacity for growth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Version 2: "optimization"&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// added capacity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop still works. Indexed assignment into &lt;code&gt;results[i]&lt;/code&gt; fills positions 0–4. No change in behavior yet.&lt;/p&gt;

&lt;p&gt;Now, a separate change: the indexed loop gets refactored. Maybe the loop structure changes, maybe someone decides &lt;code&gt;append&lt;/code&gt; is more idiomatic, maybe the jobs slice changes to a channel. The replacement looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Version 3: refactored loop using append&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles. This runs. And this produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// results: [0 0 0 0 0 r0 r1 r2 r3 r4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five zero-value holes at the front, followed by the actual results. The slice now has length 10 instead of 5. Any code downstream that iterates &lt;code&gt;results&lt;/code&gt; processes 10 elements instead of 5, with the first 5 being garbage zeros.&lt;/p&gt;

&lt;p&gt;If the downstream code is a JSON response, you send extra zeros to clients. If it's an aggregation, your totals are wrong. If it's a database write, you insert rows you didn't intend to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is hard to catch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No compile error.&lt;/strong&gt; Both indexed assignment and &lt;code&gt;append&lt;/code&gt; are valid operations on a &lt;code&gt;[]int&lt;/code&gt;. The compiler has nothing to complain about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests often miss it.&lt;/strong&gt; If your test asserts &lt;code&gt;len(results) == 5&lt;/code&gt;, it fails — but if it only checks specific indices or iterates the whole slice, the zeros look like valid data. A test against a jobs list where &lt;code&gt;process&lt;/code&gt; legitimately returns 0 for some inputs will pass completely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The change looks like a safe refactor.&lt;/strong&gt; Changing &lt;code&gt;results[i] = x&lt;/code&gt; to &lt;code&gt;results = append(results, x)&lt;/code&gt; seems equivalent to anyone who hasn't internalized how &lt;code&gt;append&lt;/code&gt; interacts with a non-zero initial length. The diff is small and the intent looks the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The capacity change seems unrelated.&lt;/strong&gt; The &lt;code&gt;make([]int, 5)&lt;/code&gt; to &lt;code&gt;make([]int, 5, 10)&lt;/code&gt; change is in a different commit, possibly by a different person. The connection between that change and the later append refactor isn't obvious.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three clean patterns — pick one and be consistent
&lt;/h2&gt;

&lt;p&gt;The root cause is mixing patterns that don't compose. Each of these is correct on its own:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Length 0, append only
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Correct: length 0, append fills from the start&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// results: [r0 r1 r2 r3 r4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when you're building a slice incrementally and don't need indexed access during construction. The capacity hint &lt;code&gt;len(jobs)&lt;/code&gt; avoids reallocations without creating holes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Length N, indexed assignment only
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Correct: length N, indexed assignment fills all slots&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// results: [r0 r1 r2 r3 r4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this when you're processing a collection with known size and need random-access writes (out-of-order filling, parallel writes with sync, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: Length N with capacity, indexed assignment only
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Correct: length N, capacity M, indexed assignment for first N slots&lt;/span&gt;
&lt;span class="c"&gt;// append for subsequent elements&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Later appends go to indices N onward — intentional&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extraResults&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this only when you genuinely need both: a pre-filled region AND growth capacity. This is the rarest case and the one that most needs a comment explaining the intent.&lt;/p&gt;




&lt;h2&gt;
  
  
  The linter: makezero
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;makezero&lt;/code&gt; linter catches exactly this class of mistake. It flags &lt;code&gt;append&lt;/code&gt; calls on slices that were initialized with non-zero length, giving you a chance to examine whether the initialization and the append are both intentional.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/ashanbrown/makezero@latest
makezero ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via &lt;code&gt;golangci-lint&lt;/code&gt; (recommended — runs alongside your other linters):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .golangci.yml&lt;/span&gt;
&lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;makezero&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// makezero: append to slice results initialized with non-zero length at line N&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does NOT flag (both are intentional patterns):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Pattern 1: length 0, append is fine&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// ✅ no warning&lt;/span&gt;

&lt;span class="c"&gt;// Pattern 2: length N, no append&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;42&lt;/span&gt;  &lt;span class="c"&gt;// ✅ no warning&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The linter doesn't tell you your code is wrong — it tells you that you have a combination that &lt;em&gt;might&lt;/em&gt; be wrong and deserves a look. That's the right level of signal. The version 3 code from the bug scenario above would have triggered this warning, prompting a review of the initialization before the append refactor shipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding an explicit comment when you genuinely mix patterns
&lt;/h2&gt;

&lt;p&gt;If you have a legitimate reason to initialize with non-zero length and later append, say so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Pre-fill the first slot with a sentinel value.&lt;/span&gt;
&lt;span class="c"&gt;// Actual results are appended starting at index 1.&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sentinel&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;//nolint:makezero // intentional: sentinel at index 0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A comment here, paired with a linter suppression (&lt;code&gt;//nolint:makezero&lt;/code&gt;), tells reviewers and future maintainers that the mixed pattern is deliberate. Without it, the next person to read the code — or the next refactor — will make the same assumption the bug relied on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick mental checklist for slice initialization
&lt;/h2&gt;

&lt;p&gt;Before writing &lt;code&gt;make([]T, n, m)&lt;/code&gt; ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Will I use indexed assignment or append?&lt;/strong&gt; Pick one. If indexed, length = final size. If append, length = 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I need pre-filled slots AND append?&lt;/strong&gt; Only then use &lt;code&gt;make([]T, n, m)&lt;/code&gt; with n &amp;gt; 0. Add a comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is the capacity hint right?&lt;/strong&gt; &lt;code&gt;make([]T, 0, len(input))&lt;/code&gt; is almost always the right pattern for transforming one slice into another.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The simplest version of this rule: &lt;strong&gt;if you're using &lt;code&gt;append&lt;/code&gt;, start with length 0&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;make([]T, 0, cap)&lt;/code&gt;&lt;/strong&gt; — Use with: &lt;code&gt;append&lt;/code&gt; only. Risk if mixed: none — this is the safe default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;make([]T, n)&lt;/code&gt;&lt;/strong&gt; — Use with: indexed assignment only. Risk if mixed: none if consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;make([]T, n, m)&lt;/code&gt;&lt;/strong&gt; — Use with: indexed assignment for the first n, append after. Risk if mixed: high — easy to introduce holes during a refactor.&lt;/p&gt;

&lt;p&gt;The bug isn't exotic. It's the interaction of two individually correct operations that become incorrect when combined without awareness of how &lt;code&gt;append&lt;/code&gt; uses &lt;code&gt;len&lt;/code&gt;, not &lt;code&gt;cap&lt;/code&gt;, to decide where to write. The &lt;code&gt;makezero&lt;/code&gt; linter closes the gap that the compiler leaves open.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part of a series on Go patterns and production gotchas. Previous posts cover concurrency mistakes, typed constants, context propagation, and profiling.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why I Always Specify Types for Constants in Go (And Name the Unit Too)</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Thu, 23 Jul 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/prasadekke/why-i-always-specify-types-for-constants-in-go-and-name-the-unit-too-6g1</link>
      <guid>https://dev.to/prasadekke/why-i-always-specify-types-for-constants-in-go-and-name-the-unit-too-6g1</guid>
      <description>&lt;p&gt;Go's untyped constants are one of the language's more elegant features — until they silently do something you didn't intend. This post is about a specific class of bug that untyped numeric constants can introduce, why the fix is simpler than you might think, and a naming convention that makes the intent impossible to misread.&lt;/p&gt;




&lt;h2&gt;
  
  
  What untyped constants actually are
&lt;/h2&gt;

&lt;p&gt;When you declare a constant in Go without an explicit type, it becomes an &lt;em&gt;untyped constant&lt;/em&gt;. It doesn't have a fixed type yet — it carries a kind (integer, float, string, bool) and a value, but its actual type is deferred until it's used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;        &lt;span class="c"&gt;// untyped integer constant&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Ratio&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.95&lt;/span&gt;      &lt;span class="c"&gt;// untyped float constant&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Label&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"worker"&lt;/span&gt;  &lt;span class="c"&gt;// untyped string constant&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is genuinely useful. An untyped integer constant can be assigned to any integer type without an explicit conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt;  &lt;span class="c"&gt;// fine&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt;  &lt;span class="c"&gt;// fine&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timeout&lt;/span&gt;  &lt;span class="c"&gt;// fine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler resolves the type at the point of use. This is why you can write &lt;code&gt;make([]byte, 1024)&lt;/code&gt; without casting — &lt;code&gt;1024&lt;/code&gt; is an untyped integer constant that fits whatever integer type &lt;code&gt;make&lt;/code&gt; needs.&lt;/p&gt;

&lt;p&gt;So far so good. The problem is that this implicit resolution extends to &lt;em&gt;named types&lt;/em&gt; built on top of numeric primitives — and some of those named types carry semantic meaning that a bare number does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug: time.Duration and the silent conversion
&lt;/h2&gt;

&lt;p&gt;Here's the situation I ran into. I had a constant for a worker pool job timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;JobTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a function that used it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles. No warning. No error. Go happily passes &lt;code&gt;5&lt;/code&gt; as a &lt;code&gt;time.Duration&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem: &lt;code&gt;time.Duration&lt;/code&gt; is defined as &lt;code&gt;type Duration int64&lt;/code&gt;, measured in &lt;strong&gt;nanoseconds&lt;/strong&gt;. So &lt;code&gt;JobTimeout = 5&lt;/code&gt; becomes a 5-nanosecond timeout. The worker timer fires almost instantly. In testing against a fast local environment, the timeout path was easy to miss. Under load with a slow downstream, every job timed out almost immediately.&lt;/p&gt;

&lt;p&gt;The intent was 5 minutes. What actually ran was 5 nanoseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// What I meant&lt;/span&gt;
&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;   &lt;span class="c"&gt;// 300,000,000,000 nanoseconds&lt;/span&gt;

&lt;span class="c"&gt;// What Go silently did&lt;/span&gt;
&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// 5 nanoseconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler had no way to know my intent because I gave it no information about units. I just said "5". Go resolved it to &lt;code&gt;time.Duration(5)&lt;/code&gt; and moved on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this specific bug is hard to catch
&lt;/h2&gt;

&lt;p&gt;Two things make it dangerous:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It compiles cleanly.&lt;/strong&gt; There's no type mismatch because untyped integer constants are assignable to any integer-based named type. The type system doesn't protect you here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's invisible in fast environments.&lt;/strong&gt; A 5-nanosecond timer almost never fires before the work completes on a modern CPU. The bug only manifests under conditions (slow network, slow DB, high load) that don't appear in unit tests running against local mocks.&lt;/p&gt;

&lt;p&gt;This is the worst class of bug: no compile error, no test failure, silent wrong behavior in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  This is not just a &lt;code&gt;time.Duration&lt;/code&gt; problem
&lt;/h2&gt;

&lt;p&gt;While revisiting this post, I found several examples of the same underlying issue showing up in different forms. They are useful because they make the problem feel less like a personal mistake and more like a real edge in Go's constant model.&lt;/p&gt;

&lt;p&gt;The Go spec is precise about this behavior: operations on untyped constants keep producing untyped constants, and when different untyped numeric kinds are mixed, the result follows the spec's kind-ordering rules. That is elegant, but it means expression shape matters.&lt;/p&gt;

&lt;p&gt;One Reddit thread demonstrates this with a tiny expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="m"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those look mathematically equivalent, but in Go they do not behave the same way. In &lt;code&gt;1/2 * 0.5&lt;/code&gt;, the &lt;code&gt;1/2&lt;/code&gt; part is evaluated as an untyped integer constant first, so it becomes &lt;code&gt;0&lt;/code&gt; before the multiplication by &lt;code&gt;0.5&lt;/code&gt;. The original poster described hitting the same pattern inside a numeric algorithm, where a small-looking constant mistake created a much larger real-world error.&lt;/p&gt;

&lt;p&gt;That is the floating-point cousin of the &lt;code&gt;time.Duration&lt;/code&gt; bug. In both cases, the compiler is following the language rules. The code is the thing that failed to say what it meant.&lt;/p&gt;

&lt;p&gt;There is also an open golangci-lint issue requesting an &lt;code&gt;untypedconst&lt;/code&gt; linter. The requested check would warn when an untyped constant is passed where a defined type is expected. That is exactly the boundary this post is about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c"&gt;// untyped constant crosses into time.Duration&lt;/span&gt;
&lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first call is legal because &lt;code&gt;5&lt;/code&gt; can become a &lt;code&gt;time.Duration&lt;/code&gt;. The second call is clear because the unit is part of the expression.&lt;/p&gt;

&lt;p&gt;Other issues show adjacent sharp edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Go compiler issue discusses a huge untyped integer constant that eventually produced a constant overflow error, even though untyped constants are often described as living in an "ideal" numeric space.&lt;/li&gt;
&lt;li&gt;An older &lt;code&gt;go/types&lt;/code&gt; issue involved subtle behavior around an untyped integer constant in a shift and conversion expression.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;go-ora&lt;/code&gt; issue shows a real dependency hitting linter failures for &lt;code&gt;2147483648 (untyped int constant) overflows int&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Another Reddit thread calls out loss of precision when using untyped constants, which is the same family of problem: the value is legal, but the intended numeric semantics are easier to lose than the code suggests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson I take from these examples is not "avoid untyped constants everywhere." That would be an overcorrection. Untyped constants are one of the reasons Go numeric literals are pleasant to use.&lt;/p&gt;

&lt;p&gt;The lesson is narrower:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When a constant crosses a semantic boundary, make the semantics explicit.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Named types like &lt;code&gt;time.Duration&lt;/code&gt;, enum-like custom types, byte counts, ports, limits, and unit-bearing values are semantic boundaries. A bare number crossing one of those boundaries deserves suspicion.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix: give the constant the correct type — time.Duration
&lt;/h2&gt;

&lt;p&gt;The fix is to declare the constant with the type it is meant to have — &lt;code&gt;time.Duration&lt;/code&gt; — and to write its value with an explicit unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ Untyped int — coerced to time.Duration(5), i.e. 5 nanoseconds&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;JobTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="c"&gt;// ✅ Typed as time.Duration, value written with its unit&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;JobTimeout&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a subtlety here that is the whole point: two separate things are happening on that second line, and you need both.&lt;/p&gt;

&lt;p&gt;First, the &lt;strong&gt;type&lt;/strong&gt; is &lt;code&gt;time.Duration&lt;/code&gt;. That is what stops the constant from being silently accepted where an &lt;code&gt;int&lt;/code&gt; or some other numeric type is expected.&lt;/p&gt;

&lt;p&gt;Second, the &lt;strong&gt;value&lt;/strong&gt; is &lt;code&gt;5 * time.Minute&lt;/code&gt;, not &lt;code&gt;5&lt;/code&gt;. That is what makes the magnitude correct — 300,000,000,000 nanoseconds instead of 5.&lt;/p&gt;

&lt;p&gt;Specifying the type alone does not save you. &lt;code&gt;const JobTimeout time.Duration = 5&lt;/code&gt; compiles, is type-safe, and is &lt;em&gt;still&lt;/em&gt; 5 nanoseconds — the original bug, now wearing a type. For a unit-bearing type like &lt;code&gt;time.Duration&lt;/code&gt;, "specify the correct type" and "write the unit" are the same discipline: the only sensible way to express a &lt;code&gt;time.Duration&lt;/code&gt; magnitude is with a unit, and once you do, the constant is already a &lt;code&gt;time.Duration&lt;/code&gt; whether or not you add the annotation.&lt;/p&gt;

&lt;p&gt;So why annotate at all, if &lt;code&gt;5 * time.Minute&lt;/code&gt; is already a &lt;code&gt;time.Duration&lt;/code&gt;? For intent at the declaration and to enforce the discipline in review. &lt;code&gt;const JobTimeout time.Duration = 5 * time.Minute&lt;/code&gt; states the contract plainly — this is a duration. It does not make &lt;code&gt;= 5&lt;/code&gt; a compile error, but it makes a bare number visibly suspicious because the type and the missing unit no longer agree with the intent.&lt;/p&gt;

&lt;p&gt;Now pass the typed constant to a function that does not accept &lt;code&gt;time.Duration&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// ✅ fine&lt;/span&gt;
&lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;// ❌ compile error: cannot use JobTimeout (constant 300000000000 of type time.Duration) as int value in argument to processJob&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The typed constant carries its semantic meaning into every use site, and the compiler enforces it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Going one step further: encode the unit in the name
&lt;/h2&gt;

&lt;p&gt;Even with a typed constant, consider what happens when someone reads just the constant name at a call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They know it's a &lt;code&gt;time.Duration&lt;/code&gt; from the type, but they have to jump to the declaration to know what 5 minutes &lt;em&gt;means&lt;/em&gt; in context. Is that a reasonable timeout? Too long? Too short?&lt;/p&gt;

&lt;p&gt;The convention I've adopted: &lt;strong&gt;put the unit in the constant name when the value is a pure magnitude&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Unit in the name — intent readable at every call site&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;JobTimeoutMinutes&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Minute&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RetryDelaySeconds&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CacheTTLHours&lt;/span&gt;     &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every call site is self-explanatory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;startWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobTimeoutMinutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;scheduleRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RetryDelaySeconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;setCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CacheTTLHours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reader doesn't need to look at the declaration. They know the unit from the name, and if they do check the declaration, the type and the multiplier confirm it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern generalises beyond time.Duration
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;time.Duration&lt;/code&gt; is the most common place this matters, but the same principle applies to any named type that wraps a primitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Byte sizes&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaxPayloadBytes&lt;/span&gt;  &lt;span class="kt"&gt;int64&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;  &lt;span class="c"&gt;// 10 MB&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ChunkSizeKB&lt;/span&gt;      &lt;span class="kt"&gt;int64&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;

&lt;span class="c"&gt;// Counts that could be confused with durations or sizes&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;WorkerPoolSize&lt;/span&gt;   &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaxRetryCount&lt;/span&gt;    &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="c"&gt;// Custom named types in your own code&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Port&lt;/span&gt;      &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;TimeoutMS&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HTTPSPort&lt;/span&gt;    &lt;span class="n"&gt;Port&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;443&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;QueryTimeout&lt;/span&gt; &lt;span class="n"&gt;TimeoutMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule of thumb: if a constant is a magnitude of something — time, bytes, counts tied to a specific resource — give it an explicit type and encode the unit in the name. If it's a dimensionless scalar (a ratio, a flag, a pure count), the type is less critical but still worth being explicit about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about typed constants and iota?
&lt;/h2&gt;

&lt;p&gt;One more case where explicit types matter: &lt;code&gt;iota&lt;/code&gt;-based enumerations. Without a type, the values are just untyped integers and can be mixed accidentally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ Untyped iota — nothing prevents mixing State and Priority values&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;StateIdle&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt; &lt;span class="c"&gt;// 0&lt;/span&gt;
    &lt;span class="n"&gt;StateRunning&lt;/span&gt;        &lt;span class="c"&gt;// 1&lt;/span&gt;
    &lt;span class="n"&gt;StateDone&lt;/span&gt;           &lt;span class="c"&gt;// 2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PriorityLow&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt; &lt;span class="c"&gt;// 0&lt;/span&gt;
    &lt;span class="n"&gt;PriorityNormal&lt;/span&gt;        &lt;span class="c"&gt;// 1&lt;/span&gt;
    &lt;span class="n"&gt;PriorityHigh&lt;/span&gt;          &lt;span class="c"&gt;// 2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;setWorkerState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;setWorkerState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PriorityHigh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// compiles, silently wrong&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With explicit types, the compiler catches the mix-up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ✅ Typed iota — compiler enforces correct usage&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;WorkerState&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Priority&lt;/span&gt;    &lt;span class="kt"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;StateIdle&lt;/span&gt;    &lt;span class="n"&gt;WorkerState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
    &lt;span class="n"&gt;StateRunning&lt;/span&gt;
    &lt;span class="n"&gt;StateDone&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;PriorityLow&lt;/span&gt;    &lt;span class="n"&gt;Priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
    &lt;span class="n"&gt;PriorityNormal&lt;/span&gt;
    &lt;span class="n"&gt;PriorityHigh&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;setWorkerState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;WorkerState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;setWorkerState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PriorityHigh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// ❌ compile error: cannot use PriorityHigh (constant 2 of type Priority) as WorkerState value in argument to setWorkerState&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type system now enforces semantic correctness, not just structural compatibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;const Timeout = 5&lt;/code&gt; passed to a &lt;code&gt;time.Duration&lt;/code&gt; param&lt;/strong&gt; — Risk: silent nanosecond conversion. Fix: &lt;code&gt;const TimeoutMinutes time.Duration = 5 * time.Minute&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;const Size = 1024&lt;/code&gt; passed to a byte-count param&lt;/strong&gt; — Risk: ambiguous unit. Fix: &lt;code&gt;const MaxSizeBytes int64 = 1024&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Untyped &lt;code&gt;iota&lt;/code&gt; constants mixed across enumerations&lt;/strong&gt; — Risk: silent wrong value. Fix: an explicit named type per enumeration.&lt;/p&gt;

&lt;p&gt;Go's untyped constants exist for good reasons — they make numeric literals flexible and reduce casting noise. But that flexibility comes with a cost: the compiler can't infer your &lt;em&gt;intent&lt;/em&gt;, only your &lt;em&gt;value&lt;/em&gt;. When a constant represents a quantity with a unit, make the unit explicit in both the type and the name. The compiler becomes your ally instead of a silent bystander.&lt;/p&gt;

&lt;p&gt;The bug that prompted this post took me an embarrassingly long time to track down. A 5-nanosecond timer that "worked fine in testing" is the kind of thing that ages you. Explicit types and unit-named constants are cheap insurance against it.&lt;/p&gt;




&lt;h2&gt;
  
  
  References and related examples
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://go.dev/ref/spec#Constant_expressions" rel="noopener noreferrer"&gt;The Go specification: Constant expressions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://go.dev/blog/constants" rel="noopener noreferrer"&gt;The Go blog: Constants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/golang/comments/1je3qtl/interesting_gotcha_with_untyped_numeric_constants/" rel="noopener noreferrer"&gt;Interesting gotcha with untyped numeric constants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/golang/comments/1gqj4ht/footgun_loss_of_precision_when_using_untyped/" rel="noopener noreferrer"&gt;Footgun: loss of precision when using untyped constants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/golangci/golangci-lint/issues/3478" rel="noopener noreferrer"&gt;golangci-lint issue: Add &lt;code&gt;untypedconst&lt;/code&gt; linter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/golang/go/issues/66776" rel="noopener noreferrer"&gt;Go issue #66776: untyped int constant gives me an overflow error&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/golang/go/issues/5849" rel="noopener noreferrer"&gt;Go issue #5849: incorrect type reported for untyped constant in conversion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/sijms/go-ora/issues/597" rel="noopener noreferrer"&gt;go-ora issue #597: untyped int constant overflows int&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is part of a series on Go patterns and production gotchas. Previous posts cover concurrency mistakes, context propagation, interface design, and profiling with pprof.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>programming</category>
      <category>backend</category>
    </item>
    <item>
      <title>Container-Aware Resource Management in Go: The Problem, Go 1.25’s Fix, and What’s Still Missing</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Tue, 21 Jul 2026 16:35:24 +0000</pubDate>
      <link>https://dev.to/prasadekke/container-aware-resource-management-in-go-the-problem-go-125s-fix-and-whats-still-missing-5gjm</link>
      <guid>https://dev.to/prasadekke/container-aware-resource-management-in-go-the-problem-go-125s-fix-and-whats-still-missing-5gjm</guid>
      <description>&lt;p&gt;If you’ve deployed a Go service to Kubernetes and set careful CPU and memory limits on your pod, there’s a good chance your Go runtime was ignoring them entirely — spawning too many threads, running GC on the wrong schedule, and getting throttled or OOM-killed as a result.&lt;/p&gt;

&lt;p&gt;Go 1.25 fixed half of this problem natively. This post explains both halves: what was broken, what’s fixed, what’s still not fixed, and what to do if you can’t upgrade yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: Go doesn’t know it’s in a container
&lt;/h2&gt;

&lt;p&gt;When a Go program starts, the runtime reads the host machine’s CPU count and sets &lt;code&gt;GOMAXPROCS&lt;/code&gt; — the number of OS threads that can run Go code in parallel — to that number. On a 64-core Kubernetes node, your Go service thinks it has 64 CPUs available, even if your pod’s resource limit is &lt;code&gt;cpu: "2"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The same problem exists for memory. Go’s garbage collector uses a target heap growth ratio (controlled by &lt;code&gt;GOGC&lt;/code&gt;) to decide when to trigger collection. By default, it allows the heap to double before collecting. If your container has a 512 MB memory limit and the Go runtime doesn’t know about it, the GC may allow heap growth that pushes your process past the cgroup limit before it triggers a collection. The result: an OOM kill.&lt;/p&gt;

&lt;p&gt;These are two distinct problems with different severities.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: GOMAXPROCS ignores cgroup CPU limits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What actually happens
&lt;/h3&gt;

&lt;p&gt;Container runtimes use Linux cgroups to enforce CPU limits. A Kubernetes &lt;code&gt;cpu: "2"&lt;/code&gt; limit translates into a cgroup CPU bandwidth quota: the container gets 2 CPU-seconds of time per second, spread across all threads.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;GOMAXPROCS&lt;/code&gt; is set to 64 (the node’s core count), your Go service creates up to 64 OS threads competing for 2 CPUs worth of time. The kernel enforces the quota by &lt;strong&gt;throttling&lt;/strong&gt; — when the cgroup exhausts its CPU time budget for the current period (typically 100ms), the kernel pauses &lt;em&gt;all&lt;/em&gt; threads in the container until the next period begins.&lt;/p&gt;

&lt;p&gt;Throttling is brutal. It’s not gentle scheduling slowdown. It’s a hard pause of your entire process. Under this scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A p99 latency that should be 10ms can spike to hundreds of milliseconds&lt;/li&gt;
&lt;li&gt;The Go GC, which runs its own goroutines, can exhaust the CPU budget and cause application threads to stall&lt;/li&gt;
&lt;li&gt;Context switching overhead multiplies — 64 threads competing for 2 CPUs thrash the scheduler
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node: 64 cores
Pod CPU limit: 2 cores
GOMAXPROCS (before 1.25): 64   ← runtime sees the host
Effective CPU available: 2     ← kernel enforces this

Result: 64 threads, 2 CPUs, aggressive throttling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real-world benchmarks have shown CPU wait times reaching tens of seconds under this misconfiguration on high-core-count nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Go 1.25 fix
&lt;/h3&gt;

&lt;p&gt;Go 1.25 makes &lt;code&gt;GOMAXPROCS&lt;/code&gt; container-aware by default on Linux. At startup, the runtime reads the cgroup CPU bandwidth limit and sets &lt;code&gt;GOMAXPROCS&lt;/code&gt; to the lower of the usual non-container default and the container’s CPU limit. Fractional CPU quotas are rounded up because &lt;code&gt;GOMAXPROCS&lt;/code&gt; is an integer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node: 64 cores
Pod CPU limit: 2 cores
GOMAXPROCS (Go 1.25): 2   ← runtime reads the cgroup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two additional behaviors from the official release notes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic updates:&lt;/strong&gt; &lt;code&gt;GOMAXPROCS&lt;/code&gt; is now periodically re-evaluated at runtime. If Kubernetes adjusts your pod’s CPU limit on the fly (via VPA or manual edit), the Go runtime will pick up the change automatically — no restart needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opt-out:&lt;/strong&gt; both behaviors can be disabled via &lt;code&gt;GODEBUG&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Disable cgroup CPU awareness at startup&lt;/span&gt;
&lt;span class="nv"&gt;GODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;containermaxprocs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="c"&gt;# Disable periodic GOMAXPROCS updates&lt;/span&gt;
&lt;span class="nv"&gt;GODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;updatemaxprocs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Manual override still works:&lt;/strong&gt; if you set &lt;code&gt;GOMAXPROCS&lt;/code&gt; via the environment variable or a &lt;code&gt;runtime.GOMAXPROCS()&lt;/code&gt; call, Go 1.25’s automatic behavior is completely disabled. Your explicit value takes precedence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One edge case worth noting:&lt;/strong&gt; Go 1.25 respects &lt;code&gt;cpu: limits&lt;/code&gt; in Kubernetes but not &lt;code&gt;cpu: requests&lt;/code&gt;. The cgroup CPU quota corresponds to the limit, not the request. If you set requests but not limits (a common pattern), Go 1.25’s container awareness has nothing to read and defaults to the host core count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.5"&lt;/span&gt;    &lt;span class="c1"&gt;# Go 1.25 ignores this&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;      &lt;span class="c1"&gt;# Go 1.25 reads this ✅&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Problem 2: GOMEMLIMIT still doesn’t read cgroup memory limits (not fixed in 1.25)
&lt;/h2&gt;

&lt;p&gt;This is the important accuracy note. Go 1.25 &lt;strong&gt;only&lt;/strong&gt; fixed CPU (&lt;code&gt;GOMAXPROCS&lt;/code&gt;). Memory awareness via &lt;code&gt;GOMEMLIMIT&lt;/code&gt; is a separate proposal (golang/go issue #75164) that has not yet shipped. As of Go 1.25, &lt;code&gt;GOMEMLIMIT&lt;/code&gt; still defaults to “unlimited” — the Go runtime has no built-in mechanism to read the container’s memory limit from cgroups and apply it automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GOMEMLIMIT&lt;/code&gt; (introduced in Go 1.19) is a soft memory limit for the Go runtime. When heap usage approaches the limit, the GC becomes more aggressive to stay within it. Without it set to your container’s memory limit, the GC may allow heap growth that triggers the kernel OOM killer before the GC intervenes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container memory limit: 512 MB
GOMEMLIMIT default: math.MaxInt64 (unlimited)

Go heap target (GOGC=100): doubles before collecting
If live heap grows past 512 MB: OOM kill before GC triggers

With GOMEMLIMIT=450MB:
GC triggers more aggressively as heap approaches 450 MB
OOM kills become rare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Workaround 1: Set GOMEMLIMIT as an environment variable
&lt;/h3&gt;

&lt;p&gt;The simplest approach. Set it in your Kubernetes deployment to slightly below your memory limit — leaving headroom for non-heap memory (stack space, runtime overhead, cgo memory):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Kubernetes pod spec&lt;/span&gt;
&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GOMEMLIMIT&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;450MiB"&lt;/span&gt;   &lt;span class="c1"&gt;# 512MB limit minus ~12% headroom&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;512Mi"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use the Kubernetes Downward API to derive it from the actual limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GOMEMLIMIT&lt;/span&gt;
    &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resourceFieldRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;containerName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-service&lt;/span&gt;
        &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;limits.memory&lt;/span&gt;
        &lt;span class="na"&gt;divisor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;   &lt;span class="c1"&gt;# bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Caveat:&lt;/strong&gt; the Downward API injects the raw memory limit, giving the runtime 0% headroom. For most services this is fine — &lt;code&gt;GOMEMLIMIT&lt;/code&gt; is a soft limit; the runtime won’t OOM itself trying to hit it. But memory-mapped files, cgo allocations, and runtime overhead sit outside the Go heap and don’t count against &lt;code&gt;GOMEMLIMIT&lt;/code&gt;. Leave 10–15% headroom if your service uses any of these.&lt;/p&gt;

&lt;h3&gt;
  
  
  Workaround 2: Set GOMEMLIMIT in code at startup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"runtime/debug"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Set to 90% of container memory limit&lt;/span&gt;
    &lt;span class="c"&gt;// Replace with your actual limit or read from env&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;memLimitBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;450&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt; &lt;span class="c"&gt;// 450 MB&lt;/span&gt;
    &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetMemoryLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memLimitBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// ... rest of main&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works but hardcodes the value, which is brittle across environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Third-party libraries: pre-1.25 CPU fix and memory fix
&lt;/h2&gt;

&lt;p&gt;If you can’t upgrade to Go 1.25, or if you need memory awareness that 1.25 doesn’t provide, two libraries solve these problems cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  uber-go/automaxprocs — CPU (pre-1.25)
&lt;/h3&gt;

&lt;p&gt;The library that Go 1.25 made redundant for most users. A single blank import sets &lt;code&gt;GOMAXPROCS&lt;/code&gt; to the container’s CPU limit at startup by reading cgroup v1 and v2 files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"go.uber.org/automaxprocs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the entire integration. The &lt;code&gt;init()&lt;/code&gt; function runs at startup, reads the cgroup CPU quota, calculates the effective CPU count, and calls &lt;code&gt;runtime.GOMAXPROCS()&lt;/code&gt;. If no cgroup limit is found, it leaves &lt;code&gt;GOMAXPROCS&lt;/code&gt; at the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to still use it:&lt;/strong&gt; if you’re on Go 1.24 or earlier. Go 1.25 reads both cgroup v1 and v2, so on Go 1.25+ you’re covered natively regardless of cgroup version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration path:&lt;/strong&gt; upgrade to Go 1.25 and remove the import. Several major projects have done exactly this.&lt;/p&gt;

&lt;h3&gt;
  
  
  KimMachineGun/automemlimit — Memory (still relevant on all versions)
&lt;/h3&gt;

&lt;p&gt;Since &lt;code&gt;GOMEMLIMIT&lt;/code&gt; container awareness isn’t in Go 1.25, this library fills the gap. It reads the cgroup memory limit and sets &lt;code&gt;GOMEMLIMIT&lt;/code&gt; automatically at startup, with configurable headroom.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"github.com/KimMachineGun/automemlimit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Default behavior: sets &lt;code&gt;GOMEMLIMIT&lt;/code&gt; to 90% of the cgroup memory limit, leaving 10% headroom for non-heap allocations. The ratio is configurable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/KimMachineGun/automemlimit/memlimit"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Set to 85% of container memory limit&lt;/span&gt;
    &lt;span class="n"&gt;memlimit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetGoMemLimitWithOpts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;memlimit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithRatio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;memlimit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memlimit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromCgroup&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supports cgroup v1 and v2. Falls back gracefully if no cgroup limit is found (leaves &lt;code&gt;GOMEMLIMIT&lt;/code&gt; unchanged).&lt;/p&gt;

&lt;h3&gt;
  
  
  tprasadtp/go-autotune — Both CPU and memory, cgroup v2 + Windows
&lt;/h3&gt;

&lt;p&gt;A newer library that handles both &lt;code&gt;GOMAXPROCS&lt;/code&gt; and &lt;code&gt;GOMEMLIMIT&lt;/code&gt; in a single import, with support for Windows via the Job Objects API in addition to Linux cgroups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"github.com/tprasadtp/go-autotune"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Constraint:&lt;/strong&gt; cgroup v2 only on Linux. If you’re on older infrastructure still running cgroup v1 (pre-Kubernetes 1.25, RHEL 8 and below), use &lt;code&gt;automaxprocs&lt;/code&gt; + &lt;code&gt;automemlimit&lt;/code&gt; instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to use and when
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Go 1.25+, Linux, Kubernetes 1.25+&lt;/strong&gt; — CPU: built-in. Memory: use &lt;code&gt;automemlimit&lt;/code&gt; or set &lt;code&gt;GOMEMLIMIT&lt;/code&gt; manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go 1.24 or earlier&lt;/strong&gt; — CPU: &lt;code&gt;automaxprocs&lt;/code&gt;. Memory: &lt;code&gt;automemlimit&lt;/code&gt; or the &lt;code&gt;GOMEMLIMIT&lt;/code&gt; env var.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go 1.25+, cgroup v2, need memory automation&lt;/strong&gt; — CPU: built-in. Memory: &lt;code&gt;automemlimit&lt;/code&gt; or &lt;code&gt;go-autotune&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go 1.25+, want explicit control&lt;/strong&gt; — CPU: &lt;code&gt;GODEBUG=containermaxprocs=0&lt;/code&gt; plus a manual value. Memory: &lt;code&gt;GOMEMLIMIT&lt;/code&gt; env var.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-Linux (Windows containers)&lt;/strong&gt; — CPU: &lt;code&gt;go-autotune&lt;/code&gt;. Memory: &lt;code&gt;go-autotune&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical Kubernetes setup for a Go 1.25 service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-service&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-service:latest&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;256Mi"&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;       &lt;span class="c1"&gt;# Go 1.25 reads this for GOMAXPROCS&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;512Mi"&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# GOMAXPROCS: handled automatically by Go 1.25&lt;/span&gt;
            &lt;span class="c1"&gt;# GOMEMLIMIT: still needs to leave headroom for non-heap memory&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GOMEMLIMIT&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;450MiB"&lt;/span&gt;  &lt;span class="c1"&gt;# 512Mi limit minus roughly 12% headroom&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, in &lt;code&gt;main.go&lt;/code&gt;, use &lt;code&gt;automemlimit&lt;/code&gt; instead of setting &lt;code&gt;GOMEMLIMIT&lt;/code&gt; manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"github.com/KimMachineGun/automemlimit"&lt;/span&gt; &lt;span class="c"&gt;// sets GOMEMLIMIT to 90% of cgroup limit&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// GOMAXPROCS handled by Go 1.25 runtime automatically.&lt;/span&gt;
    &lt;span class="c"&gt;// GOMEMLIMIT handled by automemlimit with its default 10% headroom.&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The container resource problem in Go has two parts: CPU throttling from a mismatched &lt;code&gt;GOMAXPROCS&lt;/code&gt;, and OOM kills from a GC that doesn’t know its memory budget. Go 1.25 solves the CPU side natively and dynamically. The memory side remains a manual step — either via the &lt;code&gt;GOMEMLIMIT&lt;/code&gt; environment variable, the Kubernetes Downward API, or &lt;code&gt;automemlimit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you’re running Go 1.25 on Kubernetes with CPU limits set, you can remove &lt;code&gt;automaxprocs&lt;/code&gt; and get the same behavior for free. For memory, &lt;code&gt;automemlimit&lt;/code&gt; remains the cleanest solution until the Go team ships the cgroup memory proposal.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is part of a series on Go in production. Previous posts cover goroutine leaks, context propagation, and profiling with pprof.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Next: Async Rust vs Go goroutines — the mental model shift.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>programming</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Profiling a Go Service in Production: pprof in 10 Minutes</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Tue, 30 Jun 2026 15:09:51 +0000</pubDate>
      <link>https://dev.to/prasadekke/profiling-a-go-service-in-production-pprof-in-10-minutes-3bl8</link>
      <guid>https://dev.to/prasadekke/profiling-a-go-service-in-production-pprof-in-10-minutes-3bl8</guid>
      <description>&lt;p&gt;Go ships with a profiler built in. No third-party tools, no agents to install, no instrumentation to add upfront. If your service imports &lt;code&gt;net/http/pprof&lt;/code&gt;, you can profile it live in production right now — CPU usage, memory allocations, goroutine counts, blocking operations. The data is available over HTTP, readable with standard Go tooling.&lt;/p&gt;

&lt;p&gt;Most engineers know pprof exists. Fewer have actually used it under pressure, on a real service, to find a real problem. This post walks through the mechanics — how to enable it, how to collect a profile, how to read a flame graph — and then shows the three classes of problems it catches most often.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enabling pprof
&lt;/h2&gt;

&lt;p&gt;For an HTTP service, one import is all it takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;

    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"net/http/pprof"&lt;/span&gt; &lt;span class="c"&gt;// registers handlers on DefaultServeMux&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Your service setup...&lt;/span&gt;

    &lt;span class="c"&gt;// pprof endpoints are now available on DefaultServeMux&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"localhost:6060"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="c"&gt;// ... rest of main&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The blank import registers the pprof HTTP handlers automatically. They’re available at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /debug/pprof/&lt;/code&gt; — index of available profiles&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /debug/pprof/goroutine&lt;/code&gt; — all current goroutines&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /debug/pprof/heap&lt;/code&gt; — memory allocations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /debug/pprof/profile?seconds=30&lt;/code&gt; — CPU profile (30-second sample)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /debug/pprof/block&lt;/code&gt; — goroutine blocking events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /debug/pprof/mutex&lt;/code&gt; — mutex contention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; bind pprof to &lt;code&gt;localhost&lt;/code&gt; only, or a private interface. Never expose it on your public port. If your service runs in Kubernetes, use &lt;code&gt;kubectl port-forward&lt;/code&gt; to reach it.&lt;/p&gt;

&lt;p&gt;For services not using &lt;code&gt;DefaultServeMux&lt;/code&gt;, register the handlers explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/debug/pprof/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pprof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/debug/pprof/cmdline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pprof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cmdline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/debug/pprof/profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pprof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/debug/pprof/symbol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pprof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/debug/pprof/trace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pprof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Trace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Collecting a CPU profile
&lt;/h2&gt;

&lt;p&gt;With the service running under load (profiling a quiet service tells you nothing useful), collect a 30-second CPU sample:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go tool pprof http://localhost:6060/debug/pprof/profile?seconds&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This downloads the profile and drops you into an interactive shell. Or skip the shell and go straight to a flame graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Collect the profile to a file&lt;/span&gt;
curl &lt;span class="nt"&gt;-o&lt;/span&gt; cpu.prof &lt;span class="s2"&gt;"http://localhost:6060/debug/pprof/profile?seconds=30"&lt;/span&gt;

&lt;span class="c"&gt;# Open the flame graph in a browser&lt;/span&gt;
go tool pprof &lt;span class="nt"&gt;-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:8080 cpu.prof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-http&lt;/code&gt; flag serves a web UI with multiple views. The flame graph is the most useful starting point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading a flame graph
&lt;/h2&gt;

&lt;p&gt;A flame graph shows where your program spends time. Each horizontal bar is a function. Width represents time — wider means more CPU. The vertical axis is the call stack — functions lower down called the ones above them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;|          processJob          |     |  encode  |
|       fetchFromDB      |other|
|         sql.Query            |
|          net.Read            |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’re looking for wide bars near the top — those are the functions consuming the most CPU. If &lt;code&gt;json.Marshal&lt;/code&gt; is unexpectedly wide, you’re spending a lot of time serializing. If &lt;code&gt;runtime.mallocgc&lt;/code&gt; is wide, you’re allocating heavily. If &lt;code&gt;sync.Mutex.Lock&lt;/code&gt; is wide, you have lock contention.&lt;/p&gt;

&lt;p&gt;The three most common findings in worker pool services:&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: Excessive allocations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;runtime.mallocgc&lt;/code&gt; is prominent in the CPU profile. Heap profile shows high allocation rate with short-lived objects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Collect a heap profile&lt;/span&gt;
curl &lt;span class="nt"&gt;-o&lt;/span&gt; heap.prof &lt;span class="s2"&gt;"http://localhost:6060/debug/pprof/heap"&lt;/span&gt;
go tool pprof &lt;span class="nt"&gt;-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:8080 heap.prof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the heap profile, switch to “alloc_objects” or “alloc_space” view (not “inuse” — that shows what’s live, not what’s being allocated). Look for hot allocation sites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common cause in worker pools:&lt;/strong&gt; allocating a new buffer or slice on every job iteration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ Allocates a new byte slice for every job&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// use buf...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ✅ Reuse buffers with sync.Pool&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bufPool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;bufPtr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bufPool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;bufPool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bufPtr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;bufPtr&lt;/span&gt;
    &lt;span class="c"&gt;// use buf...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sync.Pool&lt;/code&gt; maintains a pool of reusable objects. Objects not retrieved before the next GC cycle are collected, so it doesn’t prevent garbage collection — it reduces the &lt;em&gt;rate&lt;/em&gt; of allocation, which reduces GC pressure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Goroutine leak
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; goroutine count grows over time and never decreases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check current goroutine count&lt;/span&gt;
curl &lt;span class="s2"&gt;"http://localhost:6060/debug/pprof/goroutine?debug=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;debug=1&lt;/code&gt; parameter returns a text listing of all goroutines with stack traces. Look for hundreds or thousands of goroutines all blocked at the same location — that’s your leak.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;goroutine 1042 [chan receive, 3 minutes]:
main.worker()
    /app/worker.go:45 +0x68
created by main.startWorkers
    /app/pool.go:23 +0x4c

goroutine 1043 [chan receive, 3 minutes]:
&lt;/span&gt;&lt;span class="c"&gt;...
&lt;/span&gt;&lt;span class="go"&gt;(997 more like this)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A thousand goroutines blocked on &lt;code&gt;chan receive&lt;/code&gt; for 3 minutes means the jobs channel was never closed. The fix is covered in the concurrency mistakes post in this series — the producer must own &lt;code&gt;close(jobs)&lt;/code&gt; via &lt;code&gt;defer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can also check the goroutine count programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// In your health check endpoint&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;healthHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"goroutines: %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NumGoroutine&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that number grows continuously over hours of runtime and never stabilizes, you have a leak.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: Lock contention
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; throughput is lower than CPU utilization suggests it should be. Workers are running but not making progress.&lt;/p&gt;

&lt;p&gt;Mutex profiling is off by default because it has overhead. Enable it from inside the process only while investigating contention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"runtime"&lt;/span&gt;

&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetMutexProfileFraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetMutexProfileFraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then collect the mutex profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; mutex.prof &lt;span class="s2"&gt;"http://localhost:6060/debug/pprof/mutex"&lt;/span&gt;
go tool pprof &lt;span class="nt"&gt;-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:8080 mutex.prof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common cause in worker pools:&lt;/strong&gt; a shared results map protected by a single mutex, written to by all workers simultaneously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ All workers contend on one mutex&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ResultCollector&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;      &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ResultCollector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Solutions, in order of preference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ✅ Option 1: Use a results channel instead of shared state&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Workers send to channel (no lock)&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="c"&gt;// Single collector goroutine reads from channel (no contention)&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;collected&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="c"&gt;// ✅ Option 2: sync.Map for concurrent read-heavy workloads&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The channel approach is idiomatic Go — share memory by communicating rather than communicating by sharing memory. The &lt;code&gt;sync.Map&lt;/code&gt; approach is better when you need random access to results across goroutines.&lt;/p&gt;




&lt;h2&gt;
  
  
  A quick reference for which profile to use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;High CPU, unclear where&lt;/strong&gt; — CPU profile: &lt;code&gt;pprof .../profile?seconds=30&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory growing over time&lt;/strong&gt; — heap profile (inuse_space): &lt;code&gt;pprof .../heap&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High GC pressure&lt;/strong&gt; — heap profile (alloc_objects): &lt;code&gt;pprof .../heap&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Goroutine count growing&lt;/strong&gt; — goroutine profile: &lt;code&gt;pprof .../goroutine?debug=1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low throughput despite low CPU&lt;/strong&gt; — mutex or block profile: &lt;code&gt;pprof .../mutex&lt;/code&gt; or &lt;code&gt;.../block&lt;/code&gt;. Enable mutex profiling with &lt;code&gt;runtime.SetMutexProfileFraction&lt;/code&gt;; enable block profiling with &lt;code&gt;runtime.SetBlockProfileRate&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  One workflow for production incidents
&lt;/h2&gt;

&lt;p&gt;When something is wrong and you need to understand it quickly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Check goroutine count (cheap, immediate)&lt;/span&gt;
curl localhost:6060/debug/pprof/goroutine?debug&lt;span class="o"&gt;=&lt;/span&gt;2 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-50&lt;/span&gt;

&lt;span class="c"&gt;# 2. If CPU is high, get a 30s CPU profile&lt;/span&gt;
curl &lt;span class="nt"&gt;-o&lt;/span&gt; cpu.prof localhost:6060/debug/pprof/profile?seconds&lt;span class="o"&gt;=&lt;/span&gt;30

&lt;span class="c"&gt;# 3. If memory is growing, get a heap snapshot&lt;/span&gt;
curl &lt;span class="nt"&gt;-o&lt;/span&gt; heap.prof localhost:6060/debug/pprof/heap

&lt;span class="c"&gt;# 4. Open whichever profile is relevant&lt;/span&gt;
go tool pprof &lt;span class="nt"&gt;-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:8080 cpu.prof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole process takes under 5 minutes. You don’t need to reproduce the problem locally, you don’t need to redeploy with extra instrumentation, and you don’t need to restart the service. The profiler runs against the live process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;pprof is built into Go’s standard library and available with a single import. The three problems it catches most reliably in worker pool services are excessive allocations (fix: &lt;code&gt;sync.Pool&lt;/code&gt;), goroutine leaks (fix: close channels, use context), and mutex contention (fix: channel-based result collection or &lt;code&gt;sync.Map&lt;/code&gt;). The flame graph view in &lt;code&gt;go tool pprof -http&lt;/code&gt; is the fastest path from profile to finding.&lt;/p&gt;

&lt;p&gt;The only prerequisite is running the service under realistic load when you collect the profile. A profile from a quiet service tells you where your code &lt;em&gt;could&lt;/em&gt; spend time, not where it &lt;em&gt;does&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous in this series: &lt;a href="https://dev.to/prasadekke/contextcontext-is-not-optional-a-practical-guide-to-cancellation-in-go-services-157m"&gt;context.Context Is Not Optional&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Next: Container-aware resource management in Go.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>programming</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>context.Context Is Not Optional: A Practical Guide to Cancellation in Go Services</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Tue, 23 Jun 2026 12:47:35 +0000</pubDate>
      <link>https://dev.to/prasadekke/contextcontext-is-not-optional-a-practical-guide-to-cancellation-in-go-services-157m</link>
      <guid>https://dev.to/prasadekke/contextcontext-is-not-optional-a-practical-guide-to-cancellation-in-go-services-157m</guid>
      <description>&lt;p&gt;Every Go service that does I/O — database calls, HTTP requests, queue polling, file reads — should be passing a &lt;code&gt;context.Context&lt;/code&gt; through every layer. In practice, a large number of codebases treat &lt;code&gt;context.Context&lt;/code&gt; as optional plumbing that gets added later, or as something only the HTTP handler layer needs to worry about.&lt;/p&gt;

&lt;p&gt;That’s the mistake. By the time you realize you need cancellation deep in a worker pool, retrofitting it is painful. This post shows what context propagation looks like in a real worker pool, what the failure modes are when you skip it, and how to do it correctly from the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  What context.Context actually does
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;context.Context&lt;/code&gt; carries three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A cancellation signal&lt;/strong&gt; — a channel that’s closed when the work should stop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A deadline or timeout&lt;/strong&gt; — an absolute time after which the context is automatically cancelled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key-value pairs&lt;/strong&gt; — request-scoped values like trace IDs (use sparingly)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a context is cancelled, any blocking operation that’s watching that context should stop and return an error. The context propagates &lt;em&gt;down&lt;/em&gt; the call stack — a parent context cancelled cancels all children. It never propagates up.&lt;/p&gt;

&lt;p&gt;The critical point: &lt;strong&gt;the context has to be passed all the way down to the code that actually blocks&lt;/strong&gt;. Holding it at the HTTP handler layer and never passing it to your database call means cancellation never reaches the work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What happens without context
&lt;/h2&gt;

&lt;p&gt;Here is a worker pool that ignores context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;runWorkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// no context&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fetchFromDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// no context&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sendToDownstream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// no context&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now consider what happens when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The calling service shuts down&lt;/li&gt;
&lt;li&gt;A client disconnects&lt;/li&gt;
&lt;li&gt;A deadline passes&lt;/li&gt;
&lt;li&gt;An operator sends SIGTERM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workers keep running. &lt;code&gt;fetchFromDatabase&lt;/code&gt; keeps waiting. &lt;code&gt;sendToDownstream&lt;/code&gt; keeps trying. The process can’t exit cleanly because goroutines are blocked on operations that have no way of knowing they should stop.&lt;/p&gt;

&lt;p&gt;In the best case, the process takes 30 seconds to shut down waiting for goroutines to unblock naturally. In the worst case, it never terminates and has to be killed, potentially leaving in-flight work in an inconsistent state.&lt;/p&gt;




&lt;h2&gt;
  
  
  The correct structure: context flows in, errors flow out
&lt;/h2&gt;

&lt;p&gt;The pattern is simple. Every function that does I/O or blocks takes a context as its &lt;strong&gt;first parameter&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;runWorkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c"&gt;// context cancelled, stop cleanly&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c"&gt;// channel closed, no more jobs&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                        &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Return first error if any&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;errs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fetchFromDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sendToDownstream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when the context is cancelled — whether by timeout, operator signal, or parent cancellation — each worker’s &lt;code&gt;select&lt;/code&gt; unblocks on &lt;code&gt;ctx.Done()&lt;/code&gt; and returns cleanly. &lt;code&gt;fetchFromDatabase&lt;/code&gt; and &lt;code&gt;sendToDownstream&lt;/code&gt; propagate the context to their underlying I/O calls (database drivers, HTTP clients), which respect it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wiring context to OS signals for clean shutdown
&lt;/h2&gt;

&lt;p&gt;The context that flows into your worker pool should ultimately be rooted at &lt;code&gt;main&lt;/code&gt;, wired to OS signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NotifyContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Interrupt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Producer&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;loadJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"producer stopped: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="c"&gt;// Workers&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;runWorkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"workers stopped with error: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"shutdown complete"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;signal.NotifyContext&lt;/code&gt; (added in Go 1.16) returns a context that’s cancelled when &lt;code&gt;SIGINT&lt;/code&gt; or &lt;code&gt;SIGTERM&lt;/code&gt; arrives. That cancellation propagates through &lt;code&gt;loadJobs&lt;/code&gt; and &lt;code&gt;runWorkers&lt;/code&gt; automatically. When the signal arrives, the producer stops generating new jobs, in-flight I/O observes cancellation through the propagated context, workers exit, and the process shuts down cleanly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Timeouts: per-job vs. pool-wide
&lt;/h2&gt;

&lt;p&gt;There are two distinct timeout requirements in a worker pool, and they need separate contexts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pool-wide timeout:&lt;/strong&gt; how long the entire batch is allowed to run. Use &lt;code&gt;context.WithTimeout&lt;/code&gt; or &lt;code&gt;context.WithDeadline&lt;/code&gt; on the root context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-job timeout:&lt;/strong&gt; how long a single job is allowed to take. Use a derived context inside the worker loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;runWorkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// ctx already carries the pool-wide deadline from the caller&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;

                    &lt;span class="c"&gt;// Per-job timeout: 30 seconds max per job&lt;/span&gt;
                    &lt;span class="n"&gt;jobCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// always call cancel to release resources&lt;/span&gt;

                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"job %s failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to note. First, &lt;code&gt;jobCtx&lt;/code&gt; is derived from &lt;code&gt;ctx&lt;/code&gt; — if the pool-wide context is cancelled, the per-job context is also cancelled automatically. The hierarchy works in your favor. Second, &lt;code&gt;cancel()&lt;/code&gt; is called immediately after &lt;code&gt;processJob&lt;/code&gt; returns, not deferred. Deferring a context cancel inside a loop means you accumulate uncancelled contexts until the goroutine exits. Call cancel immediately after the work is done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Checking context in CPU-bound work
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;ctx.Done()&lt;/code&gt; pattern works for I/O that blocks, because the underlying library propagates the context. But what about CPU-bound work — a job that does heavy computation without any I/O?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Check context periodically in long-running CPU work&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;processChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;select&lt;/code&gt; with a &lt;code&gt;default&lt;/code&gt; branch is non-blocking — it checks whether the context is done and moves on immediately if not. Checking every 100 iterations (or every N milliseconds using a ticker) is a reasonable balance between responsiveness and overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three rules
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Context is always the first parameter.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;doWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a struct field, not a global, not injected via closure. First parameter, every time. This is a Go convention strong enough to be in the standard library style guide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Never store context in a struct.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Worker&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ✅&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contexts are request-scoped. A struct outlives any single request. Storing context in a struct means you’re using the wrong context for future requests, or holding a cancelled context longer than you should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Always call cancel.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// even if the timeout fires first&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the timeout fires before you call cancel, calling cancel is a no-op. But if the work finishes before the timeout, calling cancel releases the timer resources immediately rather than waiting for the timeout to expire. It costs nothing and prevents a resource leak.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Clean shutdown on SIGTERM&lt;/strong&gt; — &lt;code&gt;signal.NotifyContext&lt;/code&gt; at &lt;code&gt;main&lt;/code&gt;, propagate down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop workers on cancellation&lt;/strong&gt; — &lt;code&gt;select { case &amp;lt;-ctx.Done(): return }&lt;/code&gt; in the worker loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-job timeout&lt;/strong&gt; — &lt;code&gt;context.WithTimeout(ctx, duration)&lt;/code&gt; inside the loop, cancel immediately after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CPU-bound work&lt;/strong&gt; — periodic &lt;code&gt;select { case &amp;lt;-ctx.Done(): return ctx.Err(); default: }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Function signatures&lt;/strong&gt; — &lt;code&gt;ctx context.Context&lt;/code&gt; is always the first parameter.&lt;/p&gt;

&lt;p&gt;Context propagation is not boilerplate. It is the mechanism that lets your service behave correctly under pressure — when deadlines pass, when clients disconnect, when operators need to restart the service without waiting for goroutines to unblock naturally. The cost of adding it from the start is a few extra parameters. The cost of retrofitting it later is much higher.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous in this series: Three Go Concurrency Mistakes I See in Almost Every Worker Pool&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Next: Profiling a Go service in production with pprof.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>backend</category>
      <category>programming</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Three Go Concurrency Mistakes I See in Almost Every Worker Pool</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Tue, 16 Jun 2026 12:42:10 +0000</pubDate>
      <link>https://dev.to/prasadekke/three-go-concurrency-mistakes-i-see-in-almost-every-worker-pool-587e</link>
      <guid>https://dev.to/prasadekke/three-go-concurrency-mistakes-i-see-in-almost-every-worker-pool-587e</guid>
      <description>&lt;p&gt;Go makes concurrency feel easy. Goroutines are cheap, channels are built into the language, and the standard library gives you everything you need to spin up a worker pool in under 30 lines. That ease is genuinely valuable — but it also means bugs sneak in quietly. The program compiles, the tests pass, and the mistake only surfaces under load or after hours of runtime.&lt;/p&gt;

&lt;p&gt;After working on high-throughput backend systems in Go, I keep seeing the same three mistakes. None of them are obscure. All of them are fixable in minutes once you know what to look for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 1: Goroutine leaks from channels that never close
&lt;/h2&gt;

&lt;p&gt;Here is a worker pool that looks completely reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;startWorkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each worker ranges over the &lt;code&gt;jobs&lt;/code&gt; channel and processes whatever comes in. Clean, idiomatic Go. The problem: &lt;strong&gt;if &lt;code&gt;jobs&lt;/code&gt; is never closed, every one of those goroutines blocks forever&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;for range&lt;/code&gt; on a channel blocks until the channel is closed or a value arrives. If the producer stops sending but never calls &lt;code&gt;close(jobs)&lt;/code&gt;, your 10 workers sit there permanently — holding memory, holding stack space, invisible to your metrics unless you’re explicitly tracking goroutine count.&lt;/p&gt;

&lt;p&gt;In a long-running service, this compounds. Every time you restart the worker pool (say, on a config reload or a new batch) without draining the old one, you accumulate leaked goroutines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix: always close the jobs channel when the producer is done, and own that responsibility clearly.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;runBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobList&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobList&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c"&gt;// Producer owns the channel and closes it when done&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// guaranteed even if we panic&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobList&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth noting here. First, &lt;code&gt;defer close(jobs)&lt;/code&gt; in the producer goroutine means the channel closes even if the producer exits early or panics while sending jobs. Second, the &lt;code&gt;sync.WaitGroup&lt;/code&gt; gives us a clean join point — &lt;code&gt;wg.Wait()&lt;/code&gt; blocks until every worker has finished draining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick diagnostic:&lt;/strong&gt; add &lt;code&gt;runtime.NumGoroutine()&lt;/code&gt; to your health endpoint. If that number grows over the lifetime of your service and never comes back down, you have a leak.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 2: WaitGroup counter manipulation in the wrong place
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sync.WaitGroup&lt;/code&gt; has one sharp edge: &lt;strong&gt;&lt;code&gt;wg.Add&lt;/code&gt; must be called before the goroutine starts, not inside it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This version has a race condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c"&gt;// ❌ too late&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the loop spawns goroutines quickly and the scheduler runs slowly, &lt;code&gt;wg.Wait()&lt;/code&gt; can be reached before any goroutine has called &lt;code&gt;wg.Add(1)&lt;/code&gt;. The counter is zero, &lt;code&gt;Wait()&lt;/code&gt; returns immediately, and your main function exits while workers are still running — silently dropping work.&lt;/p&gt;

&lt;p&gt;This is a classic race condition. It won’t always reproduce, which makes it worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is simple: call &lt;code&gt;wg.Add&lt;/code&gt; in the loop body, before launching the goroutine.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// ✅ happens-before the goroutine starts&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the counter increment is guaranteed to happen before &lt;code&gt;wg.Wait()&lt;/code&gt; could possibly observe a zero. The goroutine carries the &lt;code&gt;Done()&lt;/code&gt; responsibility via &lt;code&gt;defer&lt;/code&gt;, so it fires even on panic.&lt;/p&gt;

&lt;p&gt;A secondary pattern worth knowing: if you know the count upfront (say, you’re launching exactly N workers), call &lt;code&gt;wg.Add(N)&lt;/code&gt; once before the loop. That’s even cleaner and harder to get wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;numWorkers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
&lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numWorkers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// add all at once&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;numWorkers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mistake 3: Unidirectional channel types ignored on function boundaries
&lt;/h2&gt;

&lt;p&gt;Go lets you declare channels as send-only (&lt;code&gt;chan&amp;lt;- Job&lt;/code&gt;) or receive-only (&lt;code&gt;&amp;lt;-chan Job&lt;/code&gt;). Most codebases I’ve seen don’t use this consistently — functions just accept &lt;code&gt;chan Job&lt;/code&gt; everywhere. This is technically fine, but it loses something real: &lt;strong&gt;the compiler can no longer tell you when your data flow is wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a worker pool where the direction confusion causes a real bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;spawnWorkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nextJob&lt;/span&gt;  &lt;span class="c"&gt;// ❌ worker feeding back into the input channel&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A worker accidentally wrote back into &lt;code&gt;jobs&lt;/code&gt; instead of &lt;code&gt;results&lt;/code&gt;. With bidirectional channels, this compiles without complaint. With directional types, it fails at compile time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;spawnWorkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nextJob&lt;/span&gt;  &lt;span class="c"&gt;// ✅ compile error: cannot send to receive-only channel&lt;/span&gt;
                &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The directional types act as documentation that’s enforced by the compiler. The producer only sees &lt;code&gt;chan&amp;lt;- Job&lt;/code&gt;. The consumer only sees &lt;code&gt;&amp;lt;-chan Job&lt;/code&gt;. Neither can accidentally do the wrong thing.&lt;/p&gt;

&lt;p&gt;A complete worker pool using this pattern correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobList&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobList&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobList&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;consume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jobList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Close results once all workers are done&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every function now only sees what it’s supposed to see. The compiler enforces the data flow. And the pattern is easy to read: &lt;code&gt;produce&lt;/code&gt; feeds in, &lt;code&gt;consume&lt;/code&gt; drains, the closer goroutine shuts down results when all workers finish.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Channel never closed&lt;/strong&gt; — What goes wrong: goroutines leak, memory grows. Fix: the producer owns &lt;code&gt;close(ch)&lt;/code&gt;, always via &lt;code&gt;defer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;wg.Add&lt;/code&gt; inside the goroutine&lt;/strong&gt; — What goes wrong: work silently dropped, races at shutdown. Fix: call &lt;code&gt;wg.Add&lt;/code&gt; before &lt;code&gt;go func()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bidirectional channels everywhere&lt;/strong&gt; — What goes wrong: wrong-direction sends compile silently. Fix: use &lt;code&gt;chan&amp;lt;-&lt;/code&gt; and &lt;code&gt;&amp;lt;-chan&lt;/code&gt; at function boundaries.&lt;/p&gt;

&lt;p&gt;None of these require clever solutions. They require habit. Once you internalize that the producer owns close, that Add precedes launch, and that channel direction is free documentation, they stop appearing.&lt;/p&gt;

&lt;p&gt;Go 1.25’s &lt;code&gt;go vet&lt;/code&gt; includes a WaitGroup analyzer that flags mistake 2 when &lt;code&gt;Add&lt;/code&gt; is called from inside a goroutine. On older Go versions, test the behavior directly by asserting that all launched work completes; &lt;code&gt;go test -race&lt;/code&gt; is not reliable for this case because &lt;code&gt;WaitGroup&lt;/code&gt; uses internal synchronization. For mistake 1, &lt;code&gt;runtime.NumGoroutine()&lt;/code&gt; in a test that runs your pool repeatedly and checks for growth is the simplest approach. Mistake 3 is pure compiler enforcement — no test needed once the types are right.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this was useful, the next post in this series covers &lt;code&gt;context.Context&lt;/code&gt; propagation in worker pools — specifically what happens when you need to cancel in-flight jobs cleanly without leaking the workers themselves.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>backend</category>
      <category>go</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Go Interfaces: Why Less Is Almost Always More</title>
      <dc:creator>Prasad Ekke</dc:creator>
      <pubDate>Mon, 08 Jun 2026 16:28:10 +0000</pubDate>
      <link>https://dev.to/prasadekke/go-interfaces-why-less-is-almost-always-more-2e48</link>
      <guid>https://dev.to/prasadekke/go-interfaces-why-less-is-almost-always-more-2e48</guid>
      <description>&lt;h2&gt;
  
  
  Go Interfaces: Why Less Is Almost Always More
&lt;/h2&gt;

&lt;p&gt;If you’re coming to Go from Java or C++, interfaces look familiar at first glance. You define a set of methods, types implement them, you write code against the interface. Same idea, right?&lt;/p&gt;

&lt;p&gt;Not quite. Go interfaces have one property that changes everything about how you should design them: &lt;strong&gt;a type implements an interface implicitly, without declaring it&lt;/strong&gt;. There’s no &lt;code&gt;implements&lt;/code&gt; keyword. If your type has the right methods, it satisfies the interface — whether it knows about the interface or not.&lt;/p&gt;

&lt;p&gt;That single property pushes you toward a design philosophy that trips up most engineers coming from other languages: &lt;strong&gt;interfaces should be small, and they should be defined by the consumer, not the producer&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the standard library is trying to tell you
&lt;/h2&gt;

&lt;p&gt;Look at the most-used interfaces in Go’s standard library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Reader&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Writer&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Closer&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One method each. That’s not laziness — that’s a deliberate design decision. Because interfaces are satisfied implicitly, a one-method interface can be satisfied by an enormous range of types: files, network connections, buffers, custom types you haven’t written yet. The smaller the interface, the more things satisfy it, the more reusable your code becomes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;io.ReadWriter&lt;/code&gt; combines two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ReadWriter&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reader&lt;/span&gt;
    &lt;span class="n"&gt;Writer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;io.ReadWriteCloser&lt;/code&gt; combines three. Notice that the standard library builds up complexity by &lt;em&gt;composing&lt;/em&gt; small interfaces, not by defining large ones upfront.&lt;/p&gt;

&lt;p&gt;This is the pattern worth internalizing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fat interface trap
&lt;/h2&gt;

&lt;p&gt;Here is how engineers from Java or C++ backgrounds tend to define interfaces in Go. Say you’re building a job processing system and you want to abstract over different job stores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ The fat interface&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;JobStore&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AddJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;GetJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;DeleteJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;ListJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;UpdateJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;CountJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;MarkComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This feels complete. It covers everything the store can do. But now ask: how many types in your codebase will ever implement all 8 methods? Probably one — your real database implementation. And your test mock has to implement all 8 even if the function under test only calls &lt;code&gt;GetJob&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Every test mock must implement all 8 methods even if irrelevant&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;mockStore&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;AddJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;                         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DeleteJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;                   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ListJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;UpdateJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;                     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;CountJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;MarkComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;                &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight methods of boilerplate for a test that calls one. And if you add a 9th method to the interface, every mock in the entire codebase breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Go way: consumer-defined, minimal interfaces
&lt;/h2&gt;

&lt;p&gt;Instead of defining one big interface at the store level, define small interfaces at each &lt;em&gt;use site&lt;/em&gt; — the function that actually needs the behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ✅ Each function declares exactly what it needs&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;JobGetter&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GetJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;JobAdder&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AddJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;JobCompleter&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;MarkComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;processJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="n"&gt;JobGetter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// process...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;submitJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="n"&gt;JobAdder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;processJob&lt;/code&gt; only depends on &lt;code&gt;JobGetter&lt;/code&gt;. Its test mock is one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;mockGetter&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;mockGetter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;GetJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your real &lt;code&gt;PostgresJobStore&lt;/code&gt; still implements all 8 methods. It satisfies every one of these small interfaces automatically, without any change, because Go interfaces are implicit. The store doesn’t know or care about &lt;code&gt;JobGetter&lt;/code&gt; or &lt;code&gt;JobAdder&lt;/code&gt; — it just has the methods, and that’s enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Accepting interfaces, returning structs
&lt;/h2&gt;

&lt;p&gt;There’s a corollary rule in Go that follows directly from this: &lt;strong&gt;accept interfaces, return concrete types&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ✅ Accept an interface — flexible, testable&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;drainQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="n"&gt;JobAdder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ❌ Return an interface — forces callers to type-assert, hides information&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newJobStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;JobStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;PostgresJobStore&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ✅ Return a concrete type — callers get the full picture&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newJobStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;PostgresJobStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;PostgresJobStore&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you return a concrete type, callers can always choose to assign it to a narrower interface themselves. When you return a fat interface, you’ve already made that decision for them, and they’re stuck with it.&lt;/p&gt;

&lt;p&gt;The one exception: returning &lt;code&gt;error&lt;/code&gt; is an interface, and that’s intentional — it lets you return any error type, including custom ones. But &lt;code&gt;error&lt;/code&gt; is a one-method interface, so the rule still holds in spirit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Composing small interfaces when you need more
&lt;/h2&gt;

&lt;p&gt;Sometimes a function genuinely needs multiple behaviors. Rather than reaching for a fat interface, compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;JobProcessor&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;JobGetter&lt;/span&gt;
    &lt;span class="n"&gt;JobCompleter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;runWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="n"&gt;JobProcessor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;doWork&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JobProcessor&lt;/code&gt; is defined at the call site, composed from two smaller interfaces, and &lt;code&gt;PostgresJobStore&lt;/code&gt; satisfies it without modification. The test mock now only needs three methods.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Interface size&lt;/strong&gt; — Avoid: 8-method interfaces defined upfront. Prefer: 1–3 method interfaces defined at the use site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where to define&lt;/strong&gt; — Avoid: the producer side (the package that has the struct). Prefer: the consumer side (the package that calls the function).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to return&lt;/strong&gt; — Avoid: interfaces from constructors. Prefer: concrete types from constructors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complexity&lt;/strong&gt; — Avoid: a single large interface. Prefer: composition of small interfaces.&lt;/p&gt;

&lt;p&gt;Go’s implicit interface satisfaction is not just a syntactic convenience — it’s the mechanism that makes all of this work without coordination between packages. A type in one package can satisfy an interface defined in a completely different package, written years later, with no coupling between them.&lt;/p&gt;

&lt;p&gt;The standard library’s one-method interfaces aren’t minimal because Go is simple. They’re minimal because the designers understood what implicit satisfaction makes possible.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Next in this series: Three Go concurrency mistakes I see in almost every worker pool.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>designpatterns</category>
    </item>
  </channel>
</rss>
