<?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: Sam Gozman</title>
    <description>The latest articles on DEV Community by Sam Gozman (@samgozman).</description>
    <link>https://dev.to/samgozman</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%2F1184263%2Fa54c06d5-10ad-4288-b2b0-30d1d7bbeb6f.png</url>
      <title>DEV Community: Sam Gozman</title>
      <link>https://dev.to/samgozman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/samgozman"/>
    <language>en</language>
    <item>
      <title>Temporal workflows in Golang: the 3 things that bite in production</title>
      <dc:creator>Sam Gozman</dc:creator>
      <pubDate>Fri, 03 Jul 2026 13:37:29 +0000</pubDate>
      <link>https://dev.to/samgozman/temporal-workflows-in-golang-the-3-things-that-bite-in-production-3jam</link>
      <guid>https://dev.to/samgozman/temporal-workflows-in-golang-the-3-things-that-bite-in-production-3jam</guid>
      <description>&lt;p&gt;I like Temporal. It's the rare orchestration tool that delivers on durable execution, and I've leaned on it for real work. But after running it in production for a while, I've noticed that the bugs that hurt are almost never the ones the Go compiler catches. They slip past &lt;code&gt;go build&lt;/code&gt;, past &lt;code&gt;go vet&lt;/code&gt;, past code review, and then they pick the worst possible moment to show up.&lt;/p&gt;

&lt;p&gt;There are three classes of these that I keep running into, roughly in order of how often they ruin someone's afternoon:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Workers panicking on bad activity calls, because the SDK erases your types.&lt;/li&gt;
&lt;li&gt;Workflows breaking replay determinism.&lt;/li&gt;
&lt;li&gt;Event history bloating until a worker runs out of memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two of the three now have tooling. One is pure architecture and no linter will save you from it. I'll go through all three: what causes it, why it's invisible until runtime, and what actually fixes it. Part of the first section is about a linter I wrote, but this post is really about the failure modes, not the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The compiler can't see your activity calls.
&lt;/h2&gt;

&lt;p&gt;Here's the signature behind most of the trouble:&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;ExecuteActivity&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;activity&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;args&lt;/span&gt; &lt;span class="o"&gt;...&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;Future&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything after &lt;code&gt;ctx&lt;/code&gt; is untyped. The SDK has no idea what &lt;code&gt;activity&lt;/code&gt; is or what arguments it expects, so it can't warn you when you get it wrong. Take a normal activity and a workflow that calls it with one argument missing:&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="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="n"&gt;Activities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Greet&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;name&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Hello, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;name&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="n"&gt;GreetWorkflow&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;workflow&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="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="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="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithActivityOptions&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;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActivityOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;StartToCloseTimeout&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;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;greeting&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="c"&gt;// Greet wants (name string). We pass nothing.&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;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecuteActivity&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;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Greet&lt;/span&gt;&lt;span class="p"&gt;)&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="n"&gt;ctx&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;greeting&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;greeting&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles. It looks fine in review. Then it runs, and the worker panics because &lt;code&gt;Greet&lt;/code&gt; expected one argument and got zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a runtime panic is worse than it sounds.
&lt;/h3&gt;

&lt;p&gt;When workflow code panics, what happens next depends on the worker's &lt;code&gt;WorkflowPanicPolicy&lt;/code&gt;. The default is &lt;code&gt;BlockWorkflow&lt;/code&gt;, and it does not fail the workflow. It puts the workflow task into a retry loop and leaves it there, on the assumption that you'll notice, fix the bug, and redeploy. The other option, &lt;code&gt;FailWorkflow&lt;/code&gt;, fails the execution immediately, which is handy in development and dangerous in production, because one bad deploy can fail every open workflow at once.&lt;/p&gt;

&lt;p&gt;So with the safe default, a missing argument doesn't fail one run and move on. It wedges &lt;strong&gt;every execution that reaches that line&lt;/strong&gt;, and they all sit there retrying until you ship a fix. The workflow isn't dead, it's stuck, which is somehow more annoying.&lt;/p&gt;

&lt;p&gt;This is a static fact about your code. There's no reason to learn about it from a stuck worker at 2am.&lt;/p&gt;

&lt;h3&gt;
  
  
  A linter for the type safety the SDK throws away.
&lt;/h3&gt;

&lt;p&gt;This is the part I got tired of, so I wrote &lt;a href="https://github.com/samgozman/temporalcheck-lint" rel="noopener noreferrer"&gt;temporalcheck-lint&lt;/a&gt;, a golangci-lint module plugin that recovers the checks &lt;code&gt;interface{}&lt;/code&gt; erases. The core analyzer is &lt;code&gt;execargs&lt;/code&gt;. It resolves the real signature of the function behind &lt;code&gt;ExecuteActivity&lt;/code&gt;, &lt;code&gt;ExecuteChildWorkflow&lt;/code&gt; and &lt;code&gt;ExecuteLocalActivity&lt;/code&gt;, then checks your call against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;workflow.go:14  ExecuteActivity: activity "Greet" expects 1 argument, got 0 (arity)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Argument count is checked by default because it's never a false positive. The type-level checks are stricter and opt-in: &lt;code&gt;strict-types&lt;/code&gt; flags a wrong argument type, &lt;code&gt;strict-pointers&lt;/code&gt; flags &lt;code&gt;T&lt;/code&gt; vs &lt;code&gt;*T&lt;/code&gt; mismatches the &lt;code&gt;DataConverter&lt;/code&gt; hides, &lt;code&gt;strict-struct-shape&lt;/code&gt; flags the wrong struct, and &lt;code&gt;strict-tests&lt;/code&gt; extends the same arity checking to your &lt;code&gt;OnActivity&lt;/code&gt; / &lt;code&gt;OnWorkflow&lt;/code&gt; mock matchers so the tests can't drift from the real signatures.&lt;/p&gt;

&lt;p&gt;There's one thing to fix first. &lt;code&gt;execargs&lt;/code&gt; can only check a target it can resolve, and a target named by its registered string is opaque:&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;// stringtarget flags this. The string can't be checked against a signature.&lt;/span&gt;
&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecuteActivity&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="s"&gt;"Greet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// Use a function reference instead, which execargs can resolve.&lt;/span&gt;
&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExecuteActivity&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;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Greet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So enable &lt;code&gt;stringtarget&lt;/code&gt;, replace string targets with function references, and the rest of the plugin can suddenly see far more. The remaining analyzers cover the quieter mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;optionsdiscard&lt;/code&gt; flags a &lt;code&gt;WithActivityOptions&lt;/code&gt; / &lt;code&gt;WithChildOptions&lt;/code&gt; call whose returned context you throw away, so the options never apply.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;optionscontext&lt;/code&gt; flags using a context built by the wrong helper, like configuring child-workflow options and then running an activity with that context.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;activitytimeout&lt;/code&gt; flags an &lt;code&gt;ActivityOptions&lt;/code&gt; literal with no &lt;code&gt;StartToCloseTimeout&lt;/code&gt; or &lt;code&gt;ScheduleToCloseTimeout&lt;/code&gt;, which Temporal rejects at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;futureget&lt;/code&gt; flags a &lt;code&gt;Future.Get&lt;/code&gt; whose returned error you discard, silently swallowing an activity or child-workflow failure.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;continueasnew&lt;/code&gt; flags a &lt;code&gt;NewContinueAsNewError&lt;/code&gt; result that's dropped instead of returned, so the workflow quietly ends instead of continuing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lossynumber&lt;/code&gt; flags &lt;code&gt;any&lt;/code&gt; / &lt;code&gt;map[string]any&lt;/code&gt; / &lt;code&gt;[]any&lt;/code&gt; parameters, where the JSON converter decodes every number as &lt;code&gt;float64&lt;/code&gt; and an &lt;code&gt;int64&lt;/code&gt; past 2^53 loses its low bits.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nonserializable&lt;/code&gt; flags &lt;code&gt;chan&lt;/code&gt; and &lt;code&gt;func&lt;/code&gt; parameters the &lt;code&gt;DataConverter&lt;/code&gt; can't serialize at all.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;workeroptions&lt;/code&gt; flags a &lt;code&gt;worker.Options&lt;/code&gt; literal that sets a &lt;code&gt;MaxConcurrentWorkflowTask*&lt;/code&gt; field to &lt;code&gt;1&lt;/code&gt;, which panics the worker on start.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Installing it is the standard golangci-lint module-plugin dance: a &lt;code&gt;.custom-gcl.yml&lt;/code&gt; listing the plugin, &lt;code&gt;golangci-lint custom&lt;/code&gt; to build a custom binary, then enable &lt;code&gt;temporalcheck&lt;/code&gt; in &lt;code&gt;.golangci.yml&lt;/code&gt;. The README has the full config with every strict toggle. The same binary plugs into GoLand and CI like the stock one.&lt;/p&gt;

&lt;p&gt;That covers the type-safety class. It does nothing for the next one.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Replay determinism.
&lt;/h2&gt;

&lt;p&gt;Temporal workflows have to be deterministic, and the reason is replay. The engine doesn't snapshot your workflow's memory. It records an event history and, whenever a worker needs to resume a workflow, it re-runs your workflow code from the top and feeds it that history to rebuild state. If your code does something that produces a different result the second time through, the replay diverges from the recorded history and you get a non-determinism error.&lt;/p&gt;

&lt;p&gt;The usual culprits are the things that change between runs: &lt;code&gt;time.Now&lt;/code&gt;, &lt;code&gt;time.Sleep&lt;/code&gt;, &lt;code&gt;math/rand&lt;/code&gt;, &lt;code&gt;crypto/rand&lt;/code&gt;, reading the &lt;code&gt;os&lt;/code&gt; standard streams. Plus a few Go constructs that are non-deterministic by nature: starting a goroutine, sending or receiving on a channel, ranging over a channel, and ranging over a map. The map one catches people, because Go randomizes map iteration order on purpose, so any logic that depends on that order replays differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  workflowcheck for determinism across the call graph.
&lt;/h3&gt;

&lt;p&gt;Temporal ships a static analyzer for exactly this, &lt;a href="https://pkg.go.dev/go.temporal.io/sdk/contrib/tools/workflowcheck" rel="noopener noreferrer"&gt;&lt;code&gt;workflowcheck&lt;/code&gt;&lt;/a&gt;. It analyzes every function registered with &lt;code&gt;RegisterWorkflow&lt;/code&gt; and reports non-deterministic code, and it follows the call graph. If a workflow calls &lt;code&gt;Y&lt;/code&gt; which calls &lt;code&gt;Z&lt;/code&gt; and &lt;code&gt;Z&lt;/code&gt; ranges over a map, it reports the whole chain.&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;go.temporal.io/sdk/contrib/tools/workflowcheck@v0.5.0
workflowcheck ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is hierarchical, so you see why each call is a problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/path/to/workflow.go:12:2: MyWorkflow is non-deterministic, reason: calls non-deterministic function time.Now
  time.Now is non-deterministic, reason: declared non-deterministic
/path/to/workflow.go:12:2: MyWorkflow is non-deterministic, reason: calls non-deterministic function fmt.Printf
  fmt.Printf is non-deterministic, reason: accesses non-deterministic var os.Stdout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works through &lt;code&gt;go vet&lt;/code&gt; too, which is the setup I'd actually recommend you to use, since &lt;code&gt;go vet&lt;/code&gt; output is cached and most editors surface it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go vet &lt;span class="nt"&gt;-vettool&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;which workflowcheck&lt;span class="si"&gt;)&lt;/span&gt; ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;False positives can be silenced inline with a &lt;code&gt;//workflowcheck:ignore&lt;/code&gt; comment, or whitelisted globally in a config file when a flagged function is safe on the path you use.&lt;/p&gt;

&lt;p&gt;There's one gap, and it's stated at the top of its own docs: &lt;strong&gt;&lt;code&gt;workflowcheck&lt;/code&gt; does not catch global variable mutation.&lt;/strong&gt; Reliably telling a deterministic global read apart from a non-deterministic mutation is hard in the general case, so it doesn't try. That's the one spot where my linter fills in. &lt;code&gt;temporalcheck&lt;/code&gt;'s &lt;code&gt;workflowstate&lt;/code&gt; analyzer flags mutating a package-level variable from workflow code, and &lt;code&gt;workflowlogger&lt;/code&gt; flags non-replay-aware logging (a plain &lt;code&gt;log.Println&lt;/code&gt; re-emits on every replay, so one event shows up many times). The tradeoff is that those two only inspect the workflow function body and its closures, not helper calls. So the two tools cover opposite gaps: &lt;code&gt;workflowcheck&lt;/code&gt; goes deep across the call graph on the determinism it knows about, and &lt;code&gt;temporalcheck&lt;/code&gt; covers global mutation and replay logging that it skips.&lt;/p&gt;

&lt;p&gt;Both of those classes are at least detectable. The third one isn't, and it's the one that actually took down a worker for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. History size, and the OOM nobody warns you about.
&lt;/h2&gt;

&lt;p&gt;This is the failure mode with no linter. It comes from the same replay mechanism that makes determinism matter, viewed from the other side.&lt;/p&gt;

&lt;p&gt;When a worker needs to resume a workflow that isn't in its cache, because it was evicted, or the worker crashed and restarted, or it landed on a fresh worker, it downloads the &lt;strong&gt;entire event history&lt;/strong&gt; and replays it from the beginning to rebuild state. That history lives in the worker's memory while it replays. The bigger the history, the more memory each replay holds, and the deserialized object graph in memory is larger than the bytes on the wire. Run a lot of workflows per worker, evict and reload a few of the fat ones at once, and the pod hits its memory limit and gets OOM-killed.&lt;/p&gt;

&lt;p&gt;Temporal puts hard caps on history size precisely to stop this from getting unbounded. The numbers are worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single payload (each workflow and activity argument and return value) warns at &lt;strong&gt;256KB&lt;/strong&gt; and is capped at &lt;strong&gt;2MB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A single gRPC message, and a single event history transaction, are capped at &lt;strong&gt;4MB&lt;/strong&gt;. You can blow past this by scheduling many activities in one workflow task even when each individual payload is under 2MB.&lt;/li&gt;
&lt;li&gt;The whole event history warns at &lt;strong&gt;10MB&lt;/strong&gt; (or 10,240 events) and is hard-terminated at &lt;strong&gt;50MB&lt;/strong&gt; (or 51,200 events). On Temporal Cloud these are non-configurable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the practical ceiling is &lt;strong&gt;50MB of history&lt;/strong&gt;, and in my experience the worker starts feeling it well before the server terminates the workflow. That lines up with the documented reason the cap exists: loading large histories into worker memory on replay is exactly what these limits are there to prevent.&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually bloats the history.
&lt;/h3&gt;

&lt;p&gt;Two things, and they compound.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;big payloads&lt;/strong&gt;. Every activity argument and return value gets recorded in the event history. If an activity returns 500KB, you can run fewer than 100 of them before the history alone crosses 50MB, before counting any other events. Signal and Update inputs count too. The data you pass around is the data Temporal durably stores, forever, in history.&lt;/p&gt;

&lt;p&gt;The second is &lt;strong&gt;too many events&lt;/strong&gt;. Every activity, timer, signal, and child workflow adds events, each carrying its payload. A workflow that does a lot of small steps gets there a different way than one that does a few huge ones, but both walk toward the same 50MB wall.&lt;/p&gt;

&lt;p&gt;I've written before about &lt;a href="https://gozman.space/blog/long-term-pitfalls-of-using-protobuf-for-apache-kafka" rel="noopener noreferrer"&gt;hitting serialization size limits with Protobuf on Kafka topics&lt;/a&gt;, and the instinct is the same here: the moment you're routinely passing multi-megabyte blobs through a system that records everything, you're going to hit a wall. Temporal just draws the wall at 50MB and enforces it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: keep big data out of history.
&lt;/h3&gt;

&lt;p&gt;The pattern is to stop passing the data itself and start passing a reference to it. Store the blob somewhere else, pass a key through the workflow, and let the activity that needs the data fetch it by key. This is sometimes called the claim-check pattern.&lt;/p&gt;

&lt;p&gt;Where you store it depends on the size and lifetime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For large or durable blobs, put them in object storage like S3 and pass the URL or key. The 50MB-per-history problem becomes a tiny string per event.&lt;/li&gt;
&lt;li&gt;For smaller, hotter, short-lived data, Redis is a better fit, with a TTL so it cleans itself up. If you want it compact in there, the &lt;a href="https://gozman.space/blog/shrinking-redis-cache-with-msgp-and-zstd-in-golang" rel="noopener noreferrer"&gt;msgp plus zstd approach I used for shrinking Redis cache&lt;/a&gt; drops structured payloads by around 90%.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Instead of passing a 5MB report through the workflow...&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ProcessWorkflow&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;workflow&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;report&lt;/span&gt; &lt;span class="n"&gt;Report&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;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// ...store it once and pass the key.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ProcessWorkflow&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;workflow&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;reportKey&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="c"&gt;// activities fetch the blob by key from S3/Redis and return only small results&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The activity does the upload at the edge and returns a key, the next activity downloads by key, and the workflow only ever sees identifiers. History stays small, replay gets cheap again, and the worker stops falling over. As a lighter measure, you can also compress payloads with a custom Data Converter before they ever reach history, which buys you headroom without restructuring anything, though it doesn't change the fundamental shape of the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where ContinueAsNew fits, and where it doesn't.
&lt;/h3&gt;

&lt;p&gt;For workflows that bloat by &lt;strong&gt;event count&lt;/strong&gt; rather than payload size, the long-running ones that loop forever, Temporal's answer is &lt;code&gt;ContinueAsNew&lt;/code&gt;. It completes the current run and atomically starts a fresh one with the same Workflow ID, a new Run ID, and an empty history. You can decide when to call it with &lt;code&gt;info.GetContinueAsNewSuggested()&lt;/code&gt; instead of hardcoding a threshold.&lt;/p&gt;

&lt;p&gt;It works, but I want to be honest about the cost. &lt;code&gt;ContinueAsNew&lt;/code&gt; is itself an action that consumes server and worker resources, and on Temporal Cloud it contributes to your overall usage and bill. The plumbing also doesn't scale gracefully: once you commit to it, every new timer, activity, or child workflow you add has to be threaded through the same continue-as-new bookkeeping, and that bookkeeping is some of the most intrusive code you'll write in a workflow. So I reach for it when a workflow really needs to run forever, not as a way to paper over giant payloads. For giant payloads, the claim-check pattern is the real fix. &lt;code&gt;ContinueAsNew&lt;/code&gt; resets the event count, but if a single payload is the problem, a fresh history just fills up again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catching it at review time with an AI reviewer.
&lt;/h3&gt;

&lt;p&gt;The catch is that none of this is something a linter can check, because whether you'll blow the 50MB cap depends on data volume the compiler never sees. A workflow that fans out one activity per user account is perfectly fine for a user with three accounts and a problem for a user with three thousand. The code looks identical either way.&lt;/p&gt;

&lt;p&gt;What's worked for me is pushing this onto the AI code reviewer. I write the hard limits straight into the guardrails file the reviewer reads, the &lt;code&gt;CLAUDE.md&lt;/code&gt; or &lt;code&gt;AGENTS.md&lt;/code&gt; or whatever your tool picks up: single payload caps at 2MB, a transaction at 4MB, history warns at 10MB and dies at 50MB or 51,200 events. Then, and this is the part that actually does the work, I document the approximate data shapes inside the project itself. Things like "a user has on the order of N accounts of this type and status," or "reading accounts across all users lands somewhere in this range." Rough numbers, not exact, but enough to reason with.&lt;/p&gt;

&lt;p&gt;With both the limits and the rough cardinalities written down, the reviewer can do the arithmetic on a pull request. It multiplies the documented per-record size by the expected count and tells you a given workflow is going to schedule tens of thousands of activities in one run, or that an activity return is going to push history past the warn threshold once real data hits it. Modern models are good at this kind of back-of-the-envelope estimate, better than I expected, and it has caught a few workflows for us before they shipped. It's not a guarantee. It is the difference between finding the problem as a review comment and finding it as a 2am OOM, which is the whole game.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefer the deterministic tool.
&lt;/h3&gt;

&lt;p&gt;I want to be careful not to oversell that last trick, because it cuts against something I believe more strongly. A deterministic check beats an AI judgment every time you can have one. A linter gives the same answer on the same code today, next week, and in CI on a Friday night. An AI reviewer is a probabilistic helper that has good days and bad days, and the bad day is the day the risky workflow ships anyway. So the AI estimate is a stopgap for the one case that resists static analysis, the data-volume problem, and not a license to stop building tools for everything else.&lt;/p&gt;

&lt;p&gt;If anything, the better use of AI here is the opposite of catching bugs at review time. It's building the thing that catches them deterministically. temporalcheck-lint exists in part because I could lean on AI to move through the analyzer scaffolding, the AST matching, and the test cases far faster than I would have alone. That's the leverage worth chasing: point the model at shaping a tool that then runs the same way forever, instead of asking it to re-derive the same judgment on every pull request. The moment you catch yourself piling rule after rule into an &lt;code&gt;AGENTS.md&lt;/code&gt; and hoping the reviewer holds them all in mind, that's usually the signal the rule wants to be a linter instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together.
&lt;/h2&gt;

&lt;p&gt;Three failure modes, and they fail in three different ways. The worker panics on a bad activity call. The replay diverges on non-deterministic code. The history bloats until a worker runs out of memory. None of them are exotic. They're the ordinary mistakes that happened to be invisible until production.&lt;/p&gt;

&lt;p&gt;Two of them you can catch in CI today. Run &lt;a href="https://github.com/samgozman/temporalcheck-lint" rel="noopener noreferrer"&gt;temporalcheck-lint&lt;/a&gt; for the type-safety and panic-prevention class, and Temporal's &lt;a href="https://pkg.go.dev/go.temporal.io/sdk/contrib/tools/workflowcheck" rel="noopener noreferrer"&gt;&lt;code&gt;workflowcheck&lt;/code&gt;&lt;/a&gt; for determinism across the call graph. They cover opposite gaps, so use both. The third one is a habit rather than a tool: treat event history as expensive, keep big data out of it by reference, reach for &lt;code&gt;ContinueAsNew&lt;/code&gt; deliberately rather than reflexively, and write the limits and your rough data sizes into the file your AI reviewer reads so it can do the math you can't see at compile time.&lt;/p&gt;

&lt;p&gt;The thing all three share is that the compiler is happy and the tests pass right up until the moment they don't. Temporal gives you durable execution, but durability means it remembers every argument you ever passed and replays every line you ever wrote. It pays to assume it will.&lt;/p&gt;

&lt;p&gt;See you in the next post 👾&lt;/p&gt;

&lt;p&gt;Sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;temporalcheck-lint: &lt;a href="https://github.com/samgozman/temporalcheck-lint" rel="noopener noreferrer"&gt;https://github.com/samgozman/temporalcheck-lint&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Temporal workflowcheck: &lt;a href="https://pkg.go.dev/go.temporal.io/sdk/contrib/tools/workflowcheck" rel="noopener noreferrer"&gt;https://pkg.go.dev/go.temporal.io/sdk/contrib/tools/workflowcheck&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Workflow Execution limits: &lt;a href="https://docs.temporal.io/workflow-execution/limits" rel="noopener noreferrer"&gt;https://docs.temporal.io/workflow-execution/limits&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Self-hosted Temporal Service defaults (payload, gRPC, history limits): &lt;a href="https://docs.temporal.io/self-hosted-guide/defaults" rel="noopener noreferrer"&gt;https://docs.temporal.io/self-hosted-guide/defaults&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Troubleshoot payload and gRPC message size limit errors: &lt;a href="https://docs.temporal.io/troubleshooting/blob-size-limit-error" rel="noopener noreferrer"&gt;https://docs.temporal.io/troubleshooting/blob-size-limit-error&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Managing very long-running Workflows with Temporal: &lt;a href="https://temporal.io/blog/very-long-running-workflows" rel="noopener noreferrer"&gt;https://temporal.io/blog/very-long-running-workflows&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;WorkflowPanicPolicy (worker package): &lt;a href="https://pkg.go.dev/go.temporal.io/sdk/worker#WorkflowPanicPolicy" rel="noopener noreferrer"&gt;https://pkg.go.dev/go.temporal.io/sdk/worker#WorkflowPanicPolicy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>temporal</category>
      <category>go</category>
      <category>linter</category>
    </item>
    <item>
      <title>Context engineering for Claude Code: improving token efficiency</title>
      <dc:creator>Sam Gozman</dc:creator>
      <pubDate>Fri, 19 Jun 2026 07:52:54 +0000</pubDate>
      <link>https://dev.to/samgozman/context-engineering-for-claude-code-improving-token-efficiency-27c1</link>
      <guid>https://dev.to/samgozman/context-engineering-for-claude-code-improving-token-efficiency-27c1</guid>
      <description>&lt;p&gt;Token bills are the new cloud bills. If you run an agentic coding tool all day, you've probably watched the number climb and wondered where it all went. A lot of it goes to noise: the agent runs &lt;code&gt;go test&lt;/code&gt;, dumps four thousand tokens of passing-test noise into the context, re-reads the same 600-line file for the third time, then pastes another wall of &lt;code&gt;git status&lt;/code&gt; output. None of that is the work. It's the exhaust.&lt;/p&gt;

&lt;p&gt;Most people reach for shorter prompts first. Wrong lever. The tokens aren't in your wording, they're in the exhaust, which makes token efficiency &lt;strong&gt;a context-engineering problem more than a writing one&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I've been running two tools to cut that exhaust for a while now: &lt;a href="https://github.com/rtk-ai/rtk" rel="noopener noreferrer"&gt;rtk&lt;/a&gt; and &lt;a href="https://github.com/mksglu/context-mode" rel="noopener noreferrer"&gt;context-mode&lt;/a&gt;. And you know how I love small changes with low effort that give huge improvements in optimization. Same idea as &lt;a href="https://gozman.space/blog/shrinking-redis-cache-with-msgp-and-zstd-in-golang" rel="noopener noreferrer"&gt;compressing a Redis cache down by ~90% with zstd&lt;/a&gt;, except here the bytes you're saving are tokens. You install a thing, you adjust a setting or two, and the bill drops. This is one of those.&lt;/p&gt;

&lt;p&gt;A note on scope. Everything here is about the CLI version of Claude Code. All of these tools work with Gemini CLI and Codex too, but Claude Code is where I tested, so that's what I'll talk about.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just the bill.
&lt;/h2&gt;

&lt;p&gt;The obvious reason to trim tokens is cost. The less obvious reason is quality.&lt;/p&gt;

&lt;p&gt;Long, junk-filled context doesn't just sit there harmlessly until you hit the window limit. It actively drags the model down. Accuracy slides as the input grows, well before the window is full, and models are especially bad at using information buried in the middle of a long context. You feel it in practice: an agent that's read fifty files and three log dumps gets vague in a way it wasn't twenty minutes earlier.&lt;/p&gt;

&lt;p&gt;So trimming the exhaust does two things at once. It saves money, and it keeps the model sharper for longer. That's a rare combination, and it's why I bother.&lt;/p&gt;

&lt;h2&gt;
  
  
  rtk: compress command output before it hits the model.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rtk&lt;/code&gt; is a CLI proxy. It sits between the agent's shell commands and the model, filters out the noise, and hands back a compact version. It's a single Rust binary, so installation is boring in the good way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;rtk
rtk init &lt;span class="nt"&gt;-g&lt;/span&gt;       &lt;span class="c"&gt;# installs the hook and an RTK.md, for Claude Code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart Claude Code. That &lt;code&gt;init&lt;/code&gt; step is the "adjust some Claude settings" part. It drops a &lt;code&gt;PreToolUse&lt;/code&gt; hook that quietly rewrites your agent's Bash commands. When Claude runs &lt;code&gt;git status&lt;/code&gt;, the hook turns it into &lt;code&gt;rtk git status&lt;/code&gt; before it executes, and the agent gets the trimmed output without knowing anything changed.&lt;/p&gt;

&lt;p&gt;Under the hood it leans on four ideas: strip the boilerplate, group similar items together, truncate the parts that don't carry signal, and collapse repeated lines into counts. The effect on something chatty like a push is dramatic:&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;# git push, raw&lt;/span&gt;
Enumerating objects: 5, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Counting objects: 100% &lt;span class="o"&gt;(&lt;/span&gt;5/5&lt;span class="o"&gt;)&lt;/span&gt;, &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
Delta compression using up to 8 threads
... about a dozen more lines ...

&lt;span class="c"&gt;# rtk git push&lt;/span&gt;
ok main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent didn't need any of the enumeration chatter. It needed to know the push worked and which branch it landed on.&lt;/p&gt;

&lt;p&gt;In my own usage, &lt;code&gt;rtk&lt;/code&gt; cut &lt;strong&gt;well over 80%&lt;/strong&gt; off the tokens spent on CLI command output. I haven't hit a problem caused by it so far, but that number comes with an asterisk, and the asterisk is the whole next section.&lt;/p&gt;

&lt;h3&gt;
  
  
  One thing to know about scope.
&lt;/h3&gt;

&lt;p&gt;The hook only fires on Bash tool calls. Claude Code's built-in &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Grep&lt;/code&gt;, and &lt;code&gt;Glob&lt;/code&gt; tools don't pass through it, so they aren't compressed automatically. If you want &lt;code&gt;rtk&lt;/code&gt;'s trimming there too, use shell commands (&lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;rg&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;) or call &lt;code&gt;rtk read&lt;/code&gt; and &lt;code&gt;rtk grep&lt;/code&gt; directly. Worth knowing so you're not confused when a big file read shows up at full size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I hold rtk back.
&lt;/h2&gt;

&lt;p&gt;There's a &lt;a href="https://reddit.com/r/ClaudeCode/comments/1spiy8t/token_optimizers_for_ai_coding_agents_are/" rel="noopener noreferrer"&gt;good study floating around&lt;/a&gt; where someone benchmarked these optimizers properly, and the conclusion stuck with me: &lt;strong&gt;they can be silently dangerous&lt;/strong&gt;. The tool decides what counts as "junk," and one bad cut makes your agent quietly dumber without you ever seeing why. Nothing errors out. The answers just get a little worse, and you have no idea the filter ate the reason.&lt;/p&gt;

&lt;p&gt;That risk isn't the same for every command, and this is where you get to be smart about it.&lt;/p&gt;

&lt;p&gt;For something like &lt;code&gt;git status&lt;/code&gt;, &lt;code&gt;git log&lt;/code&gt;, or a generic log dump, the noise-to-signal ratio is terrible and the cost of a mistake is low. If the filter trims a &lt;code&gt;git log&lt;/code&gt; a bit too aggressively, the agent asks again or moves on. Fine. So I let &lt;code&gt;rtk&lt;/code&gt; handle those freely.&lt;/p&gt;

&lt;p&gt;Test runners and linters feel like the opposite, and this is where it gets interesting. When a test fails, the exact assertion and the line number are the whole point. When a linter flags a rule on a specific file, that detail is what the agent is supposed to act on. Those are exactly the things an aggressive filter is tempted to drop, because to a compressor they read like repetitive structured noise. So the obvious move is to exclude tests outright. I didn't, and I'm glad I didn't.&lt;/p&gt;

&lt;p&gt;The thing that saves you is the pass/fail asymmetry. A passing test run is pure ceremony, hundreds of lines of "ok" the model learns nothing from. A failing run is all signal. So instead of excluding tests, I lean on &lt;code&gt;rtk&lt;/code&gt;'s &lt;code&gt;tee&lt;/code&gt;: passing runs collapse to a one-liner, and the moment something fails, rtk writes the full unfiltered output to disk so the failure detail survives intact and the agent reads it without re-running anything.&lt;/p&gt;

&lt;p&gt;You'll find the config file location and its full structure in the &lt;a href="https://www.rtk-ai.app/docs/getting-started/configuration/#config-file-location" rel="noopener noreferrer"&gt;rtk docs&lt;/a&gt;. The relevant snippet for the &lt;code&gt;tee&lt;/code&gt; setting looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tee]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"failures"&lt;/span&gt;   &lt;span class="c"&gt;# keep the full output on disk whenever something breaks&lt;/span&gt;

&lt;span class="nn"&gt;[hooks]&lt;/span&gt;
&lt;span class="c"&gt;# the escape hatch: only list a command here if you catch rtk&lt;/span&gt;
&lt;span class="c"&gt;# dropping something you actually needed&lt;/span&gt;
&lt;span class="py"&gt;exclude_commands&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;That &lt;code&gt;tee&lt;/code&gt; line is the real guard, not the exclude list. It's what lets me compress the noisiest commands I run without losing the one output that ever matters. If I catch &lt;code&gt;rtk&lt;/code&gt; eating a detail I needed, the command goes in &lt;code&gt;exclude_commands&lt;/code&gt;, but so far I haven't had to put anything there.&lt;/p&gt;

&lt;h2&gt;
  
  
  context-mode: keep raw tool output out of context entirely.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rtk&lt;/code&gt; handles shell output. &lt;code&gt;context-mode&lt;/code&gt; goes after the other big offender: MCP tools.&lt;/p&gt;

&lt;p&gt;Every MCP call tends to dump its raw payload straight into the conversation. A browser snapshot, twenty fetched issues, a fat access log. It piles up fast and you lose a chunk of your window to data you looked at once. &lt;code&gt;context-mode&lt;/code&gt; is an MCP server that keeps that raw data out of the context window. It runs the work in a sandbox, stores the request and response bits in a local SQLite database, and surfaces only the slice that's actually relevant.&lt;/p&gt;

&lt;p&gt;The mental model it pushes is "think in code." Instead of reading fifty files into context to count something, the agent writes a small script that does the counting and prints just the answer. One script replaces ten tool calls, and only the result lands in the chat.&lt;/p&gt;

&lt;p&gt;It also does session continuity. As you work, it records file edits, git operations, tasks, and errors into SQLite. When the conversation compacts, it can rebuild that working state so the model picks up where you left off instead of asking you what you were doing.&lt;/p&gt;

&lt;p&gt;Install on Claude Code is through the plugin marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add mksglu/context-mode
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;context-mode@context-mode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart, then run &lt;code&gt;/context-mode:ctx-doctor&lt;/code&gt; to confirm everything wired up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learn how to turn it off before you need to.
&lt;/h3&gt;

&lt;p&gt;Here's the catch, and it's the reason I tell people to try it rather than blindly trust it. Because the raw MCP input and output never land in the chat, debugging an MCP tool gets painful. If your agent is calling some tool wrong, you can't just scroll up and read the exchange, because the exchange isn't there. It got sandboxed.&lt;/p&gt;

&lt;p&gt;So learn how to disable &lt;code&gt;context-mode&lt;/code&gt; before you actually need to. When an MCP integration is misbehaving, turn it off, debug the tool with the raw payloads visible, then turn it back on. If you wait until you're mid-incident to figure out the off switch, you'll be annoyed.&lt;/p&gt;

&lt;p&gt;One more grain of salt: the project reports reductions up to 98%. Treat numbers like that as directional. They're self-reported and they swing hard depending on your workload. Measure your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do you even need this?
&lt;/h2&gt;

&lt;p&gt;Worth asking honestly. The harnesses are catching up on their own. Claude Code already compacts context automatically and clears stale tool results, so some of what these tools do may get absorbed into the platform over time.&lt;/p&gt;

&lt;p&gt;But "premature optimization" is my usual warning, and the reason this clears that bar is the effort side, not the savings side. You're not rewriting anything or trading away readability. You install a binary, flip a couple of settings, and the work continues exactly as before, just cheaper and a bit clearer. When the cost of trying something is this low and the savings show up on day one, I'll take it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be careful what you cache.
&lt;/h2&gt;

&lt;p&gt;Prompt caching deserves its own warning here, because it's the one optimization that can quietly work against you. The pitch sounds like free money: mark a stable chunk of the prompt, pay a small premium to write it once, then read it back at a tenth of the price on every later turn. The catch is what it nudges you to do. The bigger and more frozen your context, the more you save. And the bigger and more frozen your context, the faster it rots.&lt;/p&gt;

&lt;p&gt;That's the trap. One day the agent starts missing things it caught an hour ago, and you decide the model got dumber. It didn't. You buried the signal under a pile of cheap, cached tokens and stopped noticing.&lt;/p&gt;

&lt;p&gt;It cuts both ways, too. Too little context and the model guesses, because it doesn't have what it needs. Too much and it starts seeing patterns that aren't there, dragging stale facts from twenty turns ago into the answer and hallucinating off its own noise. No flag fixes that. The whole game is keeping it balanced, and caching pushes you toward "more" precisely because more is cheaper. Cheap is not the same as good.&lt;/p&gt;

&lt;p&gt;So my honest take: don't hand-roll prompt caching unless you own the inference stack. Claude already caches the stable prefix for you, Codex does its own caching on the OpenAI side, and the rest are heading the same way. Unless you're running your own swarm of local models where you control the entire request structure, you're reinventing something the platform already does for free, and you inherit the rot along with it.&lt;/p&gt;

&lt;p&gt;The caching worth your time is the safe kind: caching what your tools and MCP servers produce, not the conversation itself. That's exactly what &lt;code&gt;rtk&lt;/code&gt; and &lt;code&gt;context-mode&lt;/code&gt; do. They keep the volatile junk out of the live window instead of freezing a stale prefix in place. Compressing a &lt;code&gt;git log&lt;/code&gt; or sandboxing a fat MCP payload can't poison the model's reasoning the way a bloated, lovingly cached context can. &lt;strong&gt;Cache the exhaust, not the conversation.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers from my setup.
&lt;/h2&gt;

&lt;p&gt;These are my own numbers, not the projects' headline figures. Two tools, two very different ways of counting, and I trust one of them more than the other.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rtk&lt;/code&gt; is the one I trust. Across roughly three thousand intercepted commands it &lt;strong&gt;saved about 83.5% of the tokens&lt;/strong&gt; that command output would otherwise have spent. The surprising part was where the savings came from. Sort the breakdown by impact and the entire top of the list is one command: &lt;code&gt;go test&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the command I was nervous about earlier, so here's the honest reconciliation. Running the suite is the noisiest thing I do, and on a passing run the output is pure ceremony the model gains nothing from, which is exactly why it compresses to almost nothing. The savings come entirely from the passes. The failures, which carry all the signal, get written out in full by &lt;code&gt;tee&lt;/code&gt;. The command I was most tempted to exclude turned into my single biggest win, precisely because compressing it throws away only noise.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;context-mode&lt;/code&gt; is the one to read with a grain of salt, because its own accounting is clearly off. Its lifetime totals are badly incomplete, undercounting my real usage by a wide margin. So I ignore the aggregate numbers it reports and look only at single sessions, and even then I trust what I see in my editor over what the dashboard claims.&lt;/p&gt;

&lt;p&gt;The per-session results swing hard, and that swing is the real story. On a long Swift and iOS session, the kind where context had grown past 280k, &lt;strong&gt;context-mode reported keeping around 96%&lt;/strong&gt; of the raw tool output out of the window, the difference between staying inside a 256k model and spilling over. On two smaller sessions, a Go code review and building a Jira reporting skill, it kept 0% out. Everything fit, so there was nothing to offload.&lt;/p&gt;

&lt;p&gt;That's not the tool failing on the small ones. It's telling the truth: the benefit scales with how much raw tool output a session produces, and most of mine don't produce much. The exception is loud and consistent. In my experience, anything Xcode-related drags in far more context and tokens than my Go work or my Jira and Notion MCP calls, by a wide margin. So the iOS session is exactly where the offloading earned its keep, and the quieter sessions are where it sat idle and did no harm.&lt;/p&gt;

&lt;p&gt;The number I actually believe isn't on any dashboard. It's that I almost never see context truncation anymore, and tasks that used to need a 1M-token window now sit comfortably inside a 256k model without it losing the plot. Most of that came from getting the MCP tool overhead out of the live context. That's the win, whatever the counter says.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cannon: headroom.
&lt;/h2&gt;

&lt;p&gt;There's a bigger weapon on the shelf, and it's worth knowing about even if you don't pick it up. &lt;a href="https://github.com/chopratejas/headroom" rel="noopener noreferrer"&gt;headroom&lt;/a&gt; takes the opposite bet from the two tools above. Instead of one job done well, it tries to be the whole compression layer: it sits between your agent and the provider and compresses everything the model reads, tool output, logs, RAG chunks, files, and the conversation history itself, before any of it lands in the window.&lt;/p&gt;

&lt;p&gt;The detail that surprised me is that it doesn't compete with &lt;code&gt;rtk&lt;/code&gt;. &lt;strong&gt;It bundles it.&lt;/strong&gt; headroom ships the &lt;code&gt;rtk&lt;/code&gt; binary for shell-output rewriting and compresses everything downstream of that. So it's a superset, not an alternative. You run it as a library, as a proxy, or with a single &lt;code&gt;headroom wrap claude&lt;/code&gt;, and it slots in front of the model.&lt;/p&gt;

&lt;p&gt;A few things set it apart from the point tools. Its compression is reversible: it keeps the originals locally and lets the model pull them back on demand, which is a real safety net that &lt;code&gt;rtk&lt;/code&gt;'s failure-only fallback doesn't match. It aligns prefixes so the provider's own cache keeps hitting instead of getting busted every turn. And it carries a shared memory across Claude, Codex, and Gemini, so several agents can draw on the same context.&lt;/p&gt;

&lt;p&gt;That last part is the tell for who it's actually for. This isn't a tool for one developer on one provider. It earns its keep when you're running multiple agents or multiple providers and want a single shared cache and memory underneath them, or when you own the inference stack and can justify the weight. And it's weight: a Python install, an ML compression model, a runtime to serve it, and an optional memory stack with real databases behind it. That's a platform, not a quick install. The aggressive, model-based compression is also the most likely to quietly drop something important, though the reversibility takes the edge off that risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up.
&lt;/h2&gt;

&lt;p&gt;Context engineering for agentic coding comes down to a simple idea: stop paying to shuttle exhaust into the model. &lt;code&gt;rtk&lt;/code&gt; trims the shell output, &lt;code&gt;context-mode&lt;/code&gt; keeps raw MCP payloads out of the window, and between them you claw back both money and a sharper model.&lt;/p&gt;

&lt;p&gt;Just be deliberate about what you let the filter throw away. Point the compression at the noisy, low-stakes stuff, keep your test and lint output honest, and know where the off switch is before you need it. Measure on your own workload, because the answer might surprise you.&lt;/p&gt;

&lt;p&gt;As for the cannon, I'm leaving it on the shelf for now. On a single provider, &lt;code&gt;rtk&lt;/code&gt; and &lt;code&gt;context-mode&lt;/code&gt; already clear a decent chunk of the waste on their own, and that's where my attention goes: quick wins, low effort, nothing I have to babysit. I'll reach for headroom when the shape of the problem changes. Once I've squeezed local models far enough that building my own little swarm at home makes sense, or once I'm running enough providers that a shared cache and a shared context become the actual bottleneck, the weight will be worth it. Not today.&lt;/p&gt;

&lt;p&gt;See you on the next one 👾&lt;/p&gt;

&lt;p&gt;Sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rtk: &lt;a href="https://github.com/rtk-ai/rtk" rel="noopener noreferrer"&gt;https://github.com/rtk-ai/rtk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;rtk docs: &lt;a href="https://www.rtk-ai.app/docs/getting-started/configuration/#config-file-location" rel="noopener noreferrer"&gt;https://www.rtk-ai.app/docs/getting-started/configuration/#config-file-location&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;context-mode: &lt;a href="https://github.com/mksglu/context-mode" rel="noopener noreferrer"&gt;https://github.com/mksglu/context-mode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;headroom: &lt;a href="https://github.com/chopratejas/headroom" rel="noopener noreferrer"&gt;https://github.com/chopratejas/headroom&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Token optimizer study: &lt;a href="https://reddit.com/r/ClaudeCode/comments/1spiy8t/token_optimizers_for_ai_coding_agents_are/" rel="noopener noreferrer"&gt;https://reddit.com/r/ClaudeCode/comments/1spiy8t/token_optimizers_for_ai_coding_agents_are/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>llm</category>
      <category>optimization</category>
    </item>
    <item>
      <title>Shrinking Redis cache with msgp and zstd in Golang</title>
      <dc:creator>Sam Gozman</dc:creator>
      <pubDate>Fri, 03 Apr 2026 15:04:53 +0000</pubDate>
      <link>https://dev.to/samgozman/shrinking-redis-cache-with-msgp-and-zstd-in-golang-57bp</link>
      <guid>https://dev.to/samgozman/shrinking-redis-cache-with-msgp-and-zstd-in-golang-57bp</guid>
      <description>&lt;p&gt;If you are storing structured data in Redis using &lt;code&gt;encoding/json&lt;/code&gt;, you might be surprised how much memory you are wasting. JSON is readable, easy to debug, and universally supported. It's also bloated. Field names repeat on every single record, numbers get stored as strings, and boolean values take 4-5 bytes instead of 1.&lt;/p&gt;

&lt;p&gt;In my previous article &lt;a href="https://gozman.space/blog/high-performance-golang-struct-optimizations-paddings-and-alignments" rel="noopener noreferrer"&gt;High-performance Golang struct optimizations: Paddings and Alignments&lt;/a&gt;, I showed how reordering struct fields can save 25% of RAM. This time, we are going after the other side of the equation: how the data is serialized before it hits Redis.&lt;/p&gt;

&lt;p&gt;I will compare four approaches: plain JSON, &lt;a href="https://github.com/tinylib/msgp" rel="noopener noreferrer"&gt;MessagePack via tinylib/msgp&lt;/a&gt;, JSON compressed with &lt;a href="https://github.com/klauspost/compress" rel="noopener noreferrer"&gt;zstd via klauspost/compress&lt;/a&gt;, and msgp + zstd combined. We care mostly about stored size here. CPU cost matters too, but for cache serialization, the bottleneck is almost always memory. The results weren't what I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The struct.
&lt;/h2&gt;

&lt;p&gt;I will reuse the struct from my &lt;a href="https://gozman.space/blog/high-performance-golang-struct-optimizations-paddings-and-alignments" rel="noopener noreferrer"&gt;previous article&lt;/a&gt;, with &lt;code&gt;msg&lt;/code&gt; tags added for msgp code generation:&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;//go:generate go tool msgp&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;NestedLayout&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;ID&lt;/span&gt;    &lt;span class="kt"&gt;int64&lt;/span&gt; &lt;span class="s"&gt;`msg:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Phone&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt; &lt;span class="s"&gt;`msg:"phone"`&lt;/span&gt;
    &lt;span class="n"&gt;Age&lt;/span&gt;   &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="s"&gt;`msg:"age"`&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;Layout&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;BalanceInCents&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;        &lt;span class="s"&gt;`msg:"balance_in_cents"`&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;        &lt;span class="s"&gt;`msg:"idempotency_key"`&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;            &lt;span class="kt"&gt;float64&lt;/span&gt;      &lt;span class="s"&gt;`msg:"key"`&lt;/span&gt;
    &lt;span class="n"&gt;User&lt;/span&gt;           &lt;span class="n"&gt;NestedLayout&lt;/span&gt; &lt;span class="s"&gt;`msg:"user"`&lt;/span&gt;
    &lt;span class="n"&gt;AreaID&lt;/span&gt;         &lt;span class="kt"&gt;int32&lt;/span&gt;        &lt;span class="s"&gt;`msg:"area_id"`&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt;      &lt;span class="kt"&gt;int32&lt;/span&gt;        &lt;span class="s"&gt;`msg:"created_at"`&lt;/span&gt;
    &lt;span class="n"&gt;UpdatedAt&lt;/span&gt;      &lt;span class="kt"&gt;int32&lt;/span&gt;        &lt;span class="s"&gt;`msg:"updated_at"`&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;             &lt;span class="kt"&gt;uint32&lt;/span&gt;       &lt;span class="s"&gt;`msg:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;         &lt;span class="kt"&gt;uint16&lt;/span&gt;       &lt;span class="s"&gt;`msg:"status"`&lt;/span&gt;
    &lt;span class="n"&gt;IsActive&lt;/span&gt;       &lt;span class="kt"&gt;bool&lt;/span&gt;         &lt;span class="s"&gt;`msg:"is_active"`&lt;/span&gt;
    &lt;span class="n"&gt;IsSpecial&lt;/span&gt;      &lt;span class="kt"&gt;bool&lt;/span&gt;         &lt;span class="s"&gt;`msg:"is_special"`&lt;/span&gt;
    &lt;span class="n"&gt;IsMigrated&lt;/span&gt;     &lt;span class="kt"&gt;bool&lt;/span&gt;         &lt;span class="s"&gt;`msg:"is_migrated"`&lt;/span&gt;
    &lt;span class="n"&gt;TenantID&lt;/span&gt;       &lt;span class="kt"&gt;int8&lt;/span&gt;         &lt;span class="s"&gt;`msg:"tenant_id"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Layouts is a named slice type.&lt;/span&gt;
&lt;span class="c"&gt;// msgp cannot generate methods on anonymous slices like []*Layout,&lt;/span&gt;
&lt;span class="c"&gt;// so we need this named type for codegen to work.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Layouts&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Layout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to note here: &lt;code&gt;msgp&lt;/code&gt; requires a named slice type to generate &lt;code&gt;MarshalMsg&lt;/code&gt; and &lt;code&gt;UnmarshalMsg&lt;/code&gt; for collections. You can't just pass &lt;code&gt;[]*Layout&lt;/code&gt; to msgp's codegen. The named type &lt;code&gt;Layouts&lt;/code&gt; solves this and lets you marshal the whole array in one call.&lt;/p&gt;

&lt;p&gt;After adding these tags, install msgp as a tool dependency and run code generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get &lt;span class="nt"&gt;-tool&lt;/span&gt; github.com/tinylib/msgp@latest
go generate ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces &lt;code&gt;*_gen.go&lt;/code&gt; files with &lt;code&gt;MarshalMsg&lt;/code&gt;, &lt;code&gt;UnmarshalMsg&lt;/code&gt;, and &lt;code&gt;Msgsize()&lt;/code&gt; methods for both types. No reflection, no runtime overhead for field lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why msgp and not protobuf or other codecs.
&lt;/h2&gt;

&lt;p&gt;I know what you are thinking. Why not protobuf? I wrote about &lt;a href="https://gozman.space/blog/long-term-pitfalls-of-using-protobuf-for-apache-kafka" rel="noopener noreferrer"&gt;pitfalls of using Protobuf for Kafka&lt;/a&gt; before, and some of those concerns apply to caching too: schema management, versioning, and the requirement to maintain &lt;code&gt;.proto&lt;/code&gt; files separately from your Go structs. For a cache layer, I want something that works directly with existing Go types.&lt;/p&gt;

&lt;p&gt;msgp generates code from Go struct tags. No separate schema files, no extra compilation step beyond &lt;code&gt;go generate&lt;/code&gt;. The generated code is fast because it produces direct binary encoding with no reflection, similar to how protobuf works at runtime but without the schema overhead.&lt;/p&gt;

&lt;p&gt;Other options like &lt;code&gt;gob&lt;/code&gt; are Go-specific and not particularly compact. &lt;code&gt;encoding/binary&lt;/code&gt; needs manual marshaling. msgp sits right where I want it: generates fast code from existing structs, no schema files required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The encoder and decoder.
&lt;/h2&gt;

&lt;p&gt;The zstd encoder and decoder are safe for concurrent use but expensive to create, so initialize them once at the package level:&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/klauspost/compress/zstd"&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;zstdEncoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWriter&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="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEncoderLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpeedFastest&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;zstdDecoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I use &lt;code&gt;zstd.SpeedFastest&lt;/code&gt; here because we are optimizing for cache throughput. The compression ratio difference between fastest and default level is small for structured data like this, but the CPU savings are noticeable at high request rates.&lt;/p&gt;

&lt;p&gt;Now here are the four serialization functions we will benchmark:&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;// 1. Plain JSON&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;encodeJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;Layouts&lt;/span&gt;&lt;span class="p"&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="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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// 2. msgp only&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;encodeMsgp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Layouts&lt;/span&gt;&lt;span class="p"&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="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;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarshalMsg&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;// 3. JSON + zstd&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;encodeJSONZstd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;Layouts&lt;/span&gt;&lt;span class="p"&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="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="n"&gt;jsonBytes&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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&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;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="no"&gt;nil&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;zstdEncoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonBytes&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="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// 4. msgp + zstd&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;encodeMsgpZstd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Layouts&lt;/span&gt;&lt;span class="p"&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="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="n"&gt;msgpBytes&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;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarshalMsg&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;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="no"&gt;nil&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;zstdEncoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msgpBytes&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="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;And decoding:&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;decodeJSON&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="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;Layouts&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;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="n"&gt;Layouts&lt;/span&gt;
    &lt;span class="k"&gt;return&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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&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="o"&gt;&amp;amp;&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;func&lt;/span&gt; &lt;span class="n"&gt;decodeMsgp&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="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;Layouts&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;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="n"&gt;Layouts&lt;/span&gt;
    &lt;span class="n"&gt;_&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnmarshalMsg&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="k"&gt;return&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;decodeJSONZstd&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="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;Layouts&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="n"&gt;decompressed&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;zstdDecoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DecodeAll&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="no"&gt;nil&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="no"&gt;nil&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="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="n"&gt;Layouts&lt;/span&gt;
    &lt;span class="k"&gt;return&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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decompressed&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;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;func&lt;/span&gt; &lt;span class="n"&gt;decodeMsgpZstd&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="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;Layouts&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="n"&gt;decompressed&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;zstdDecoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DecodeAll&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="no"&gt;nil&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="no"&gt;nil&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="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="n"&gt;Layouts&lt;/span&gt;
    &lt;span class="n"&gt;_&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnmarshalMsg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decompressed&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;result&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Benchmarks.
&lt;/h2&gt;

&lt;p&gt;Let's create a test dataset of 1000 records with realistic data and measure the output size and encoding speed for each approach:&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;generateTestData&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;Layouts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&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;Layouts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&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="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;data&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;BalanceInCents&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int64&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="m"&gt;100&lt;/span&gt; &lt;span class="o"&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;IdempotencyKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000000&lt;/span&gt; &lt;span class="o"&gt;+&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;Key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="kt"&gt;float64&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="m"&gt;1.337&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NestedLayout&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="kt"&gt;int64&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="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;Phone&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15551234567&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Age&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="m"&gt;25&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;int32&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="m"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;AreaID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="kt"&gt;int32&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="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;1700000000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;int32&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="n"&gt;UpdatedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="m"&gt;1700000000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="kt"&gt;int32&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="m"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="kt"&gt;uint32&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="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="kt"&gt;uint16&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="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;IsActive&lt;/span&gt;&lt;span class="o"&gt;:&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;2&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;IsSpecial&lt;/span&gt;&lt;span class="o"&gt;:&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;7&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;IsMigrated&lt;/span&gt;&lt;span class="o"&gt;:&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;3&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;TenantID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="kt"&gt;int8&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="m"&gt;10&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;return&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;func&lt;/span&gt; &lt;span class="n"&gt;BenchmarkEncode&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="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;generateTestData&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="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"JSON"&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;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encodeJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Msgp"&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;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encodeMsgp&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;data&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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"JSON+Zstd"&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;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encodeJSONZstd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Msgp+Zstd"&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;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encodeMsgpZstd&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;data&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;And a separate test to print the actual byte sizes, which is really the number we care 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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestOutputSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;generateTestData&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="n"&gt;jsonBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;encodeJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;msgpBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;encodeMsgp&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;jsonZstdBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;encodeJSONZstd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;msgpZstdBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;encodeMsgpZstd&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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"JSON:       %d bytes"&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;jsonBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Msgp:       %d bytes"&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;msgpBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"JSON+Zstd:  %d bytes"&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;jsonZstdBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Logf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Msgp+Zstd:  %d bytes"&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;msgpZstdBytes&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;Here are the results from my machine (Go 1.25, Apple M4 Pro). Sizes first, since that is the whole point:&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="o"&gt;===&lt;/span&gt; RUN   TestOutputSize
    t_test.go:  JSON:       256243 bytes
    t_test.go:  Msgp:       189709 bytes
    t_test.go:  JSON+Zstd:  22914 bytes
    t_test.go:  Msgp+Zstd:  27517 bytes
&lt;span class="nt"&gt;---&lt;/span&gt; PASS: TestOutputSize &lt;span class="o"&gt;(&lt;/span&gt;0.01s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JSON produces ~256 KB for 1000 records. msgp alone drops that to ~190 KB, a &lt;strong&gt;26% reduction&lt;/strong&gt;. And here is where it gets interesting: JSON+zstd compresses down to ~23 KB, but msgp+zstd lands at ~27.5 KB. &lt;strong&gt;The msgp+zstd combination is larger than JSON+zstd.&lt;/strong&gt; That wasn't what I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why msgp+zstd is bigger than JSON+zstd.
&lt;/h2&gt;

&lt;p&gt;This surprised me at first, but it makes sense once you think about how zstd works. zstd is a dictionary-based compressor. It finds repeated byte sequences and replaces them with short back-references. JSON is full of exactly that kind of redundancy: the field names &lt;code&gt;"balance_in_cents":&lt;/code&gt;, &lt;code&gt;"idempotency_key":&lt;/code&gt;, &lt;code&gt;"is_active":&lt;/code&gt; repeat verbatim for every record in the array. With 1000 records, the string &lt;code&gt;"balance_in_cents"&lt;/code&gt; appears 1000 times. zstd sees that pattern and after the first occurrence, each repetition costs almost nothing.&lt;/p&gt;

&lt;p&gt;msgp has already eliminated that redundancy. Field names are encoded as short binary keys, so there are no long repeated strings left for zstd to find. The msgp output is already compact, which paradoxically means zstd has less to work with. You end up with zstd's frame overhead on top of data that doesn't compress well.&lt;/p&gt;

&lt;p&gt;So the tradeoff is not as simple as "stack both optimizations for maximum savings." If your goal is strictly the smallest possible cache footprint and you can afford the CPU cost, JSON+zstd actually wins on size. But that is not the full picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The speed side of things.
&lt;/h2&gt;

&lt;p&gt;Here are the encoding benchmarks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BenchmarkEncode/JSON-14          4254       274337 ns/op     262699 B/op     2 allocs/op
BenchmarkEncode/Msgp-14         37448        31810 ns/op     221185 B/op     1 allocs/op
BenchmarkEncode/JSON+Zstd-14     1960       557230 ns/op     591831 B/op     3 allocs/op
BenchmarkEncode/Msgp+Zstd-14     3906       300849 ns/op     417836 B/op     2 allocs/op
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;msgp encoding is &lt;strong&gt;8.6x faster&lt;/strong&gt; than JSON. But JSON+zstd, the size winner, is also the &lt;strong&gt;slowest&lt;/strong&gt; option at 557 us/op, more than twice as slow as plain JSON. You pay for JSON's reflection-based marshaling first, then zstd's compression pass on top of a much larger input buffer.&lt;/p&gt;

&lt;p&gt;msgp+zstd at 300 us/op is almost &lt;strong&gt;2x faster&lt;/strong&gt; than JSON+zstd while producing a comparable cache size (27.5 KB vs 22.9 KB). For a cache layer handling thousands of requests per second, that speed difference matters more than 4.6 KB.&lt;/p&gt;

&lt;p&gt;And msgp alone at 31 us/op is almost &lt;strong&gt;9x faster&lt;/strong&gt; than plain JSON. If your cache data fits comfortably in Redis at 190 KB per user instead of 256 KB, that might be all you need, and you avoid the zstd dependency entirely.&lt;/p&gt;

&lt;p&gt;The auto-generated msgp benchmarks confirm the per-struct performance as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BenchmarkMarshalMsgLayout-14     32107621     36.08 ns/op    224 B/op    1 allocs/op
BenchmarkAppendMsgLayout-14      76366846     15.95 ns/op      0 B/op    0 allocs/op
BenchmarkUnmarshalLayout-14      18507248     64.46 ns/op      0 B/op    0 allocs/op
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;36 nanoseconds to marshal a single struct with 13 fields and a nested sub-struct. Zero allocations on the append path. Generated code with no reflection does well here.&lt;/p&gt;

&lt;h2&gt;
  
  
  So which one should you pick?
&lt;/h2&gt;

&lt;p&gt;It depends on what you are constrained by. Here is how I think about it:&lt;/p&gt;

&lt;p&gt;If &lt;strong&gt;Redis memory is the bottleneck&lt;/strong&gt; and you need the absolute smallest stored size, use JSON+zstd. ~23 KB per 1000 records, 91% reduction from plain JSON. You pay for it with 557 us per encode operation, which might be fine if your write rate is low.&lt;/p&gt;

&lt;p&gt;If &lt;strong&gt;CPU and latency matter&lt;/strong&gt; and you want fast serialization with a decent size reduction, use msgp alone. ~190 KB per 1000 records, 26% reduction, but 8.6x faster encoding with a single allocation. No compression dependency.&lt;/p&gt;

&lt;p&gt;If you want a &lt;strong&gt;balance between size and speed&lt;/strong&gt;, use msgp+zstd. ~27.5 KB per 1000 records, 89% reduction from plain JSON (close to JSON+zstd's 91%) at nearly 2x the encoding speed. This is probably the right default for most applications that need to optimize cache size without introducing a CPU bottleneck.&lt;/p&gt;

&lt;p&gt;What does this mean in real numbers? If you are caching data for 100,000 active users with 1000 records each, JSON will eat ~24.4 GB of Redis memory. JSON+zstd brings that to ~2.2 GB. Msgp+zstd lands at ~2.6 GB, but encodes twice as fast. Either way, that is the difference between needing a large Redis cluster and getting by with a single instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on zstd singleton.
&lt;/h2&gt;

&lt;p&gt;The first thing the klauspost/compress/zstd documentation warns about is not to create a new &lt;code&gt;zstd.NewWriter&lt;/code&gt; or &lt;code&gt;zstd.NewReader&lt;/code&gt; per request. Both are designed to be reused and are safe for concurrent access. Creating them is expensive because zstd initializes internal lookup tables and allocates buffers on construction. If you put &lt;code&gt;zstd.NewWriter(nil)&lt;/code&gt; inside your request handler, you will burn CPU on initialization that has nothing to do with your actual data.&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;// NOT this per request&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;handleRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWriter&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="c"&gt;// expensive, don't do this&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;encoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A cleaner approach is to wrap both into a struct that you initialize once and inject where needed:&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;ZstdCompressor&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;encoder&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encoder&lt;/span&gt;
    &lt;span class="n"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decoder&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;NewZstdCompressor&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="n"&gt;ZstdCompressor&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="n"&gt;encoder&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;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWriter&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="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEncoderLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpeedFastest&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithEncoderConcurrency&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="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithWindowSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithLowerEncoderMem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;true&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="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="no"&gt;nil&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;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"zstd: failed to initialize zstd encoder: %w"&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="n"&gt;decoder&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;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&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="n"&gt;zstd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithDecoderConcurrency&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="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;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"zstd: failed to initialize zstd decoder: %w"&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ZstdCompressor&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;,&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;z&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ZstdCompressor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&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="kt"&gt;byte&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;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ZstdCompressor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Decompress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&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="kt"&gt;byte&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="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;z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DecodeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&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;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="no"&gt;nil&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;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"zstd: failed to decompress: %w"&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;result&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way you handle initialization errors properly instead of swallowing them with &lt;code&gt;_&lt;/code&gt;, and the compressor can be passed around as a dependency. I'll explain the extra options in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taming zstd memory usage.
&lt;/h2&gt;

&lt;p&gt;After deploying the &lt;code&gt;ZstdCompressor&lt;/code&gt; above with default options, I noticed RAM usage on the pods grew 2.5x over a couple of hours. Disabling the feature flag stopped new calls, but memory didn't drop. Only a pod restart freed it. That was a red flag.&lt;/p&gt;

&lt;p&gt;The cause isn't a memory leak in the traditional sense. It's how the encoder manages its internal buffers. When you create an encoder with &lt;code&gt;zstd.NewWriter(nil)&lt;/code&gt;, it lazily initializes one set of internal buffers per concurrency slot. The default concurrency is &lt;code&gt;GOMAXPROCS&lt;/code&gt;, so on a 4-core pod you get 4 internal encoders, on a 16-core machine you get 16. Each slot allocates window-sized buffers (default 8 MB), plus hash tables and scratch space. All of these are kept alive for reuse after the first &lt;code&gt;EncodeAll&lt;/code&gt; call triggers their initialization. The Go garbage collector can't collect them because the singleton encoder holds references.&lt;/p&gt;

&lt;p&gt;So if you have a 16-core pod, the encoder alone can hold 16 * ~10 MB = ~160 MB of buffers that will never be released until the encoder itself is garbage collected or the process exits. Under concurrent load, every slot gets initialized and stays initialized.&lt;/p&gt;

&lt;p&gt;Three options on the encoder side fix this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithEncoderConcurrency(1)&lt;/code&gt; limits the encoder to a single internal buffer set. &lt;code&gt;EncodeAll&lt;/code&gt; runs each call on one goroutine anyway, so multiple concurrent callers just queue. For a cache layer doing 50 RPS, this is more than enough. This alone can cut encoder memory by 4-16x depending on your CPU count.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithWindowSize(1&amp;lt;&amp;lt;20)&lt;/code&gt; sets the window to 1 MB instead of the default 8 MB. For cache payloads under 200 KB, an 8 MB window is overkill. The window only needs to be larger than your largest input for compression to work at full ratio. 1 MB covers that with room to spare.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;WithLowerEncoderMem(true)&lt;/code&gt; trades a small amount of compression speed for roughly half the buffer allocations. For &lt;code&gt;SpeedFastest&lt;/code&gt; level with small payloads, the speed difference is negligible.&lt;/p&gt;

&lt;p&gt;The decoder has a similar concurrency model. Default is &lt;code&gt;min(GOMAXPROCS, 4)&lt;/code&gt; decoders, each with its own buffers. &lt;code&gt;WithDecoderConcurrency(1)&lt;/code&gt; applies the same fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  A generic cache helper.
&lt;/h2&gt;

&lt;p&gt;If you want to use this approach across different types, you can write a generic helper using Go's type constraints. The trick is the two-type-parameter pattern: Go generics cannot call pointer-receiver interface methods on &lt;code&gt;T&lt;/code&gt; directly, so you need a constraint that ties the pointer type to the value type.&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;MsgpCodec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&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="n"&gt;MarshalMsg&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="kt"&gt;byte&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;UnmarshalMsg&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="kt"&gt;byte&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;T&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;CacheSet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="n"&gt;MsgpCodec&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;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;rdb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;key&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;value&lt;/span&gt; &lt;span class="o"&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;ttl&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="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;encoded&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;PT&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarshalMsg&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;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="n"&gt;compressed&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;zstdEncoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoded&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;return&lt;/span&gt; &lt;span class="n"&gt;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&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;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;CacheGet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt; &lt;span class="n"&gt;MsgpCodec&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;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;rdb&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;key&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="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;T&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="n"&gt;val&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;rdb&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="n"&gt;ctx&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&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="no"&gt;nil&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;decompressed&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;zstdDecoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DecodeAll&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="no"&gt;nil&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="no"&gt;nil&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;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;PT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;new&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;_&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;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnmarshalMsg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decompressed&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="no"&gt;nil&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;result&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call site looks a bit verbose because of Go's generics syntax:&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;CacheSet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Layouts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Layouts&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;rdb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user:123:accounts"&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;accounts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&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;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not pretty, but it is type safe and you only write the serialization logic once.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this does not help.
&lt;/h2&gt;

&lt;p&gt;Before you get excited about 90%+ reductions, I need to be clear about what kind of data benefits from this approach. It only makes sense for serialized JSON stored in Redis. If you are caching a plain string, a boolean flag, a counter, or any other scalar value, there is nothing to optimize. Redis already stores those efficiently. A &lt;code&gt;SET user:123:name "John"&lt;/code&gt; is 4 bytes of payload. Running it through msgp + zstd would make it larger, not smaller, because of the encoding headers and compression frame overhead.&lt;/p&gt;

&lt;p&gt;The wins come from structured data: arrays of objects, nested JSON documents, anything where &lt;code&gt;encoding/json&lt;/code&gt; adds repeated field names, type coercion overhead, and structural characters like &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;, &lt;code&gt;]&lt;/code&gt;, &lt;code&gt;:&lt;/code&gt;, and &lt;code&gt;,&lt;/code&gt;. The more records in your array and the more fields in your struct, the bigger the savings. A single flat struct with 3 fields will barely compress. A thousand records with 13 fields each will compress dramatically, as we saw in the benchmarks above.&lt;/p&gt;

&lt;p&gt;So keep your regular &lt;code&gt;SET&lt;/code&gt;/&lt;code&gt;GET&lt;/code&gt; for simple values. This approach is specifically for the cases where you are serializing Go structs or slices of structs into Redis as JSON blobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats.
&lt;/h2&gt;

&lt;p&gt;A few gotchas before you switch everything to msgp + zstd.&lt;br&gt;
First, &lt;strong&gt;debugging gets harder&lt;/strong&gt;. You can't just &lt;code&gt;redis-cli GET&lt;/code&gt; a key and read the value anymore. You need a small tool to decode and decompress the data. For development and debugging, I would recommend keeping a fallback to JSON or at least having a CLI utility that can decode your cached values.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;msg tags on every field are mandatory&lt;/strong&gt;. Without them, msgp falls back to Go field names for serialization keys. This works until someone renames a struct field and silently breaks deserialization of all existing cached data. Use explicit &lt;code&gt;msg:"field_name"&lt;/code&gt; tags and treat them like database column names: once set, they should not change.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;versioning your cache keys&lt;/strong&gt; is good practice when changing serialization formats. If you switch from JSON to msgp, old cached values will fail to decode. Use a versioned key prefix like &lt;code&gt;v2:user:123:accounts&lt;/code&gt; so old and new formats can coexist during rollout.&lt;/p&gt;

&lt;p&gt;Fourth, &lt;strong&gt;zstd compression ratio depends on your data&lt;/strong&gt;. Repetitive data like arrays of similar structs compresses well. A single small struct with unique values might not shrink much, and you will pay the CPU cost for no benefit. Test with your actual data before committing.&lt;/p&gt;

&lt;p&gt;Fifth, &lt;strong&gt;this works because our struct uses simple built-in types&lt;/strong&gt;. All fields here are &lt;code&gt;int64&lt;/code&gt;, &lt;code&gt;int32&lt;/code&gt;, &lt;code&gt;float64&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt;, and so on. msgp knows how to serialize those out of the box. If your struct contains fields from external libraries, like &lt;code&gt;decimal.Decimal&lt;/code&gt;from shopspring/decimal or &lt;code&gt;uuid.UUID&lt;/code&gt; from google/uuid, msgp won't know how to encode them. You would need to implement the &lt;code&gt;msgp.Marshaler&lt;/code&gt; and &lt;code&gt;msgp.Unmarshaler&lt;/code&gt; interfaces on those types yourself, or convert the fields to primitive types before serialization (for example, storing a Decimal as a string or as cents in &lt;code&gt;int64&lt;/code&gt;). Not a dealbreaker, but worth knowing before you adopt msgp for structs with non-trivial field types.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to bother with this.
&lt;/h2&gt;

&lt;p&gt;Same as with struct padding optimizations: most applications do not need this. If your Redis usage is well within limits and you are not worried about memory costs, plain JSON works fine and is easier to debug.&lt;/p&gt;

&lt;p&gt;But if you are running into Redis memory limits, paying for oversized instances, or caching data for millions of users, a 90% size reduction is hard to ignore. Whether you pick JSON+zstd for maximum compression or msgp+zstd for the speed/size balance depends on your workload. Measure both on your actual data. The answer might surprise you, as it surprised me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How to read benchmarks and what else you can optimize in your Go application I described in my article &lt;a href="https://gozman.space/blog/optimization-odyssey-profiling-and-benchmarking-golang-app-with-pprof" rel="noopener noreferrer"&gt;Optimization Odyssey: pprof-ing &amp;amp; Benchmarking Golang App&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;tinylib/msgp: &lt;a href="https://github.com/tinylib/msgp" rel="noopener noreferrer"&gt;https://github.com/tinylib/msgp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;klauspost/compress/zstd: &lt;a href="https://github.com/klauspost/compress" rel="noopener noreferrer"&gt;https://github.com/klauspost/compress&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MessagePack specification: &lt;a href="https://msgpack.org/" rel="noopener noreferrer"&gt;https://msgpack.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Zstandard RFC 8878: &lt;a href="https://datatracker.ietf.org/doc/html/rfc8878" rel="noopener noreferrer"&gt;https://datatracker.ietf.org/doc/html/rfc8878&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>optimization</category>
      <category>caching</category>
    </item>
    <item>
      <title>High-performance Golang struct optimizations: Paddings and Alignments.</title>
      <dc:creator>Sam Gozman</dc:creator>
      <pubDate>Sat, 14 Jun 2025 19:47:01 +0000</pubDate>
      <link>https://dev.to/samgozman/high-performance-golang-struct-optimizations-paddings-and-alignments-199n</link>
      <guid>https://dev.to/samgozman/high-performance-golang-struct-optimizations-paddings-and-alignments-199n</guid>
      <description>&lt;p&gt;When we're creating structs in Golang, we typically think about the fields we need and their types, the readability and maintainability. Sometimes we sort fields by their name, sometimes by their logic, and sometimes we just put them in the order we like. But what if I tell you that the order of fields in Golang struct dramatically affects the memory usage and performance of your application?&lt;/p&gt;

&lt;p&gt;In certain scenarios, you can &lt;strong&gt;reduce RAM usage&lt;/strong&gt; by your application by &lt;strong&gt;20-50%&lt;/strong&gt; and increase allocation and access performance &lt;strong&gt;for free&lt;/strong&gt;. To achieve this, we need to understand how the Golang compiler works with paddings and alignments in structs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Struct Layout.
&lt;/h2&gt;

&lt;p&gt;The compiler in Golang uses a specified logic for struct layout to make it more efficient in terms of memory access and optimal alignment for target hardware architecture. The compiler will align struct fields to the size of the largest field type in the struct, and it will also &lt;strong&gt;add padding bytes between fields&lt;/strong&gt; to ensure that each field is aligned correctly.&lt;/p&gt;

&lt;p&gt;So if your struct has &lt;strong&gt;2 fields&lt;/strong&gt;, one of type &lt;code&gt;int32&lt;/code&gt; and another of type &lt;code&gt;int64&lt;/code&gt;, the compiler will align the &lt;code&gt;int64&lt;/code&gt; field to &lt;strong&gt;8 bytes&lt;/strong&gt; boundaries, and it will add &lt;strong&gt;4 bytes of padding&lt;/strong&gt; after the &lt;code&gt;int32&lt;/code&gt; field to ensure that the &lt;code&gt;int64&lt;/code&gt; field is aligned correctly. This 8 byte boundary size applies to 64-bit architecture, which is the most common architecture used today. 64 bits equals 8 bytes, we can say that 64-bit architecture &lt;a href="https://en.wikipedia.org/wiki/Word_(computer_architecture)" rel="noopener noreferrer"&gt;word&lt;/a&gt; size is 8 bytes and 32-bit architecture word size is 4 bytes.&lt;/p&gt;

&lt;p&gt;However, if the underlying type size is greater than the word size, the compiler will still align it to our architecture word size (8 bytes in our case). For example, if you have a string field of size &lt;strong&gt;30 bytes&lt;/strong&gt;, the compiler will align it to 8 bytes boundaries making it look like 4 chunks: 3 chunks of 8 bytes and 1 chunk of 6 bytes plus &lt;strong&gt;2 padding bytes&lt;/strong&gt; to make it &lt;strong&gt;32 bytes in total&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Those padding bytes are not used by the application, but they are still allocated in memory, which can lead to increased memory usage and decreased performance. What can you possibly put in 2 bytes? It can be 2 fields of &lt;code&gt;int8&lt;/code&gt; or &lt;code&gt;byte&lt;/code&gt; type, or even &lt;code&gt;bool&lt;/code&gt;; one field of &lt;code&gt;int16&lt;/code&gt; and &lt;code&gt;uint16&lt;/code&gt; etc.&lt;/p&gt;

&lt;p&gt;So, the order of fields in your structure will affect the number of &lt;strong&gt;padding bytes&lt;/strong&gt; added by the compiler &lt;strong&gt;between&lt;/strong&gt; them and the overall memory usage of your application. The more padding bytes you have, the more memory your application will use, and the slower it will be.&lt;/p&gt;

&lt;h2&gt;
  
  
  How paddings are born.
&lt;/h2&gt;

&lt;p&gt;Let's take a look at a simple structure example first:&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;// NestedLayout represents a nested structure simple example.&lt;/span&gt;
  &lt;span class="c"&gt;// Size: 24 bytes&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;NestedLayout&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;ID&lt;/span&gt;    &lt;span class="kt"&gt;int64&lt;/span&gt; &lt;span class="c"&gt;// 8 bytes&lt;/span&gt;
    &lt;span class="n"&gt;Phone&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt; &lt;span class="c"&gt;// 8 bytes&lt;/span&gt;
    &lt;span class="n"&gt;Age&lt;/span&gt;   &lt;span class="kt"&gt;int32&lt;/span&gt; &lt;span class="c"&gt;// 4 bytes&lt;/span&gt;
    &lt;span class="c"&gt;// 4 bytes padding to align the structure to 8 bytes&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c"&gt;// UnoptimizedLayout - not optimized for size and performance.&lt;/span&gt;
  &lt;span class="c"&gt;// Size: 96 bytes&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UnoptimizedLayout&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;AreaID&lt;/span&gt;         &lt;span class="kt"&gt;int32&lt;/span&gt;        &lt;span class="c"&gt;// 4 bytes&lt;/span&gt;
    &lt;span class="n"&gt;IsActive&lt;/span&gt;       &lt;span class="kt"&gt;bool&lt;/span&gt;         &lt;span class="c"&gt;// 1 byte&lt;/span&gt;
    &lt;span class="n"&gt;BalanceInCents&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;        &lt;span class="c"&gt;// 8 bytes&lt;/span&gt;
    &lt;span class="n"&gt;IsSpecial&lt;/span&gt;      &lt;span class="kt"&gt;bool&lt;/span&gt;         &lt;span class="c"&gt;// 1 byte&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;        &lt;span class="c"&gt;// 8 bytes&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;             &lt;span class="kt"&gt;uint32&lt;/span&gt;       &lt;span class="c"&gt;// 4 bytes&lt;/span&gt;
    &lt;span class="n"&gt;User&lt;/span&gt;           &lt;span class="n"&gt;NestedLayout&lt;/span&gt; &lt;span class="c"&gt;// 24 bytes&lt;/span&gt;
    &lt;span class="n"&gt;IsMigrated&lt;/span&gt;     &lt;span class="kt"&gt;bool&lt;/span&gt;         &lt;span class="c"&gt;// 1 byte&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt;      &lt;span class="kt"&gt;int32&lt;/span&gt;        &lt;span class="c"&gt;// 4 bytes&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;         &lt;span class="kt"&gt;uint16&lt;/span&gt;       &lt;span class="c"&gt;// 2 bytes&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;            &lt;span class="kt"&gt;float64&lt;/span&gt;      &lt;span class="c"&gt;// 8 bytes&lt;/span&gt;
    &lt;span class="n"&gt;TenantID&lt;/span&gt;       &lt;span class="kt"&gt;int8&lt;/span&gt;         &lt;span class="c"&gt;// 1 byte&lt;/span&gt;
    &lt;span class="n"&gt;UpdatedAt&lt;/span&gt;      &lt;span class="kt"&gt;int32&lt;/span&gt;        &lt;span class="c"&gt;// 4 bytes&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure &lt;code&gt;UnoptimizedLayout&lt;/code&gt; may look normal to the untrained eye, but it has a lot of padding bytes between fields. The total size of the structure is &lt;strong&gt;96 bytes after compiler&lt;/strong&gt; optimizations. But if you manually calculate the sum of all the fields you will get &lt;strong&gt;70 bytes&lt;/strong&gt;. The size of your structure increased by &lt;strong&gt;26 padding bytes&lt;/strong&gt;. You can check the size of this structure for yourself using &lt;code&gt;Sizeof&lt;/code&gt; function from &lt;code&gt;unsafe&lt;/code&gt; package:&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;fmt&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;"UnoptimizedLayout size: %d bytes&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;unsafe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UnoptimizedLayout&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;blockquote&gt;
&lt;p&gt;UnoptimizedLayout size: 96 bytes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  aligo: visualizing paddings of your structure.
&lt;/h3&gt;

&lt;p&gt;To visualize paddings and alignments of your structure, I recommend using &lt;a href="https://github.com/essentialkaos/aligo" rel="noopener noreferrer"&gt;aligo tool&lt;/a&gt;. It is a simple command line tool that can help you to visualize the layout of your structures in Golang and help you to optimize it. You can install it using &lt;code&gt;go install&lt;/code&gt; command:&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/essentialkaos/aligo/v2@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, you can run it against your structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aligo &lt;span class="nt"&gt;-s&lt;/span&gt; UnoptimizedLayout view main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will output a visual representation of your structure with paddings and alignments:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx34tq2atvujrmylu6m8h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx34tq2atvujrmylu6m8h.png" alt="aligo: UnoptimizedLayout" width="800" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Paddings marked with red color can be optimized by changing the order of fields in your structure. Let's take a look at another &lt;code&gt;aligo&lt;/code&gt; command that will help us to optimize our structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aligo &lt;span class="nt"&gt;-s&lt;/span&gt; UnoptimizedLayout check main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will output a sorted struct with optimized paddings and alignments:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fovpt74vasb3fbub5fejw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fovpt74vasb3fbub5fejw.png" alt="aligo: UnoptimizedLayout optimized" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see that we just changed the order of fields and reduced the size of our structure from 96 bytes to 72 bytes - &lt;strong&gt;25% memory usage reduction&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  aligo: comparing structures.
&lt;/h3&gt;

&lt;p&gt;Let's rename our optimized structure to &lt;code&gt;OptimizedLayout&lt;/code&gt; and run &lt;code&gt;aligo view&lt;/code&gt; to visualize the difference:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ogl11l2dui2qm9i38nx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ogl11l2dui2qm9i38nx.jpg" alt="aligo: OptimizedLayout" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see that the number of padding bytes is reduced from 26 to 2 bytes. The total size of the structure is reduced from 96 bytes to 72 bytes, which is a &lt;strong&gt;25% memory usage reduction&lt;/strong&gt;. We still have 2 padding bytes left, we can add 2 more boolean fields in our structure for free, and we will still have the same size of 72 bytes.&lt;/p&gt;

&lt;p&gt;If you have a million of those records sitting in your memory, your unoptimized structure will use &lt;strong&gt;96 MB&lt;/strong&gt; of memory, while the optimized one will use only &lt;strong&gt;72 MB&lt;/strong&gt;. This is a &lt;strong&gt;24 MB&lt;/strong&gt; difference, which is a pretty significant number for a million records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks.
&lt;/h2&gt;

&lt;p&gt;Now that we know how to optimize our structures, let's take a look at the performance difference between them.&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;Benchmark&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="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;1000000&lt;/span&gt; &lt;span class="c"&gt;// Number of records to allocate and access&lt;/span&gt;
    &lt;span class="n"&gt;unoptimized&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;UnoptimizedLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;optimized&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;OptimizedLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&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="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MemoryAllocation-UnoptimizedLayout"&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;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&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;UnoptimizedLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"MemoryAllocation-OptimizedLayout"&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;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&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;OptimizedLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"FieldAccess-UnoptimizedLayout"&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;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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;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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&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;for&lt;/span&gt; &lt;span class="n"&gt;j&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;unoptimized&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;unoptimized&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BalanceInCents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int64&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="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="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="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"FieldAccess-OptimizedLayout"&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;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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;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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Loop&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;for&lt;/span&gt; &lt;span class="n"&gt;j&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;optimized&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;optimized&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BalanceInCents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int64&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="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="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;Now run the benchmarks using &lt;code&gt;go test -bench=. -benchmem&lt;/code&gt; command and you will get similar output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Benchmark/MemoryAllocation-UnoptimizedLayout-14  2470  447593  ns/op  96002053 B/op 1 allocs/op
Benchmark/MemoryAllocation-OptimizedLayout-14    3802  314902  ns/op  72007681 B/op 1 allocs/op
Benchmark/FieldAccess-UnoptimizedLayout-14        948  1118201 ns/op  0        B/op 0 allocs/op
Benchmark/FieldAccess-OptimizedLayout-14         1202  927754  ns/op  0        B/op 0 allocs/op
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, the numbers may vary depending on your hardware and Go version, but you can see that the optimized structure has better memory allocation performance and field access performance. The memory allocation time for unoptimized structure is &lt;strong&gt;447593 ns/op&lt;/strong&gt; while for optimized structure it is &lt;strong&gt;314902 ns/op&lt;/strong&gt;. The field access time for unoptimized structure is &lt;strong&gt;1118201 ns/op&lt;/strong&gt; while for optimized structure it is &lt;strong&gt;927754 ns/op&lt;/strong&gt;. This is a &lt;strong&gt;30%&lt;/strong&gt; and &lt;strong&gt;17%&lt;/strong&gt; performance improvement respectively for 1 million records. Absolutely for free!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How to read benchmarks and what else you can bench I described in my article &lt;a href="https://gozman.space/blog/optimization-odyssey-profiling-and-benchmarking-golang-app-with-pprof" rel="noopener noreferrer"&gt;Optimization Odyssey: pprof-ing &amp;amp; Benchmarking Golang App&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The elephant in the room.
&lt;/h2&gt;

&lt;p&gt;Let's take a look at the elephant in the room. The &lt;strong&gt;elephants&lt;/strong&gt;, to be precise. Types that I never mentioned in this article on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pointer&lt;/strong&gt; - pointer to the structure in Golang only takes &lt;strong&gt;8 bytes&lt;/strong&gt; of memory on 64-bit architecture. But it obscures the real size of the structure it points to and adds additional job for the garbage collector as well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interface&lt;/strong&gt; - the interface type in Golang is a reference type, which means that it does not have a fixed size and can be used to store any type that implements the interface. We can't say for sure ho much memory it will use because it depends on the implementation. But we can say that the interface pointer in Golang is &lt;strong&gt;16 bytes&lt;/strong&gt;. Why 16 bytes and not 8? Because Golang uses &lt;strong&gt;pointer tagging&lt;/strong&gt; to store additional information about the pointer, like the type of the value it points to. This is done to make the garbage collector more efficient and to allow for more flexible memory management. So, if you have a structure with an interface field, it will use &lt;strong&gt;16 bytes&lt;/strong&gt; for the &lt;em&gt;interface&lt;/em&gt; and additional memory for the implementation it points to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;String&lt;/strong&gt; - the string type in Golang is also a reference type, which means that it does not have a fixed size and can be used to store any string value. The string type in Golang is implemented as a struct with two fields: a pointer to the string data and the length of the string. The pointer is &lt;strong&gt;8 bytes&lt;/strong&gt; (&lt;code&gt;uintptr&lt;/code&gt; type) and the length is &lt;strong&gt;8 bytes&lt;/strong&gt; (&lt;code&gt;int&lt;/code&gt; type), so the &lt;strong&gt;minimum size&lt;/strong&gt; of the string type is &lt;strong&gt;16 bytes&lt;/strong&gt;. This means that if you have a structure with a &lt;em&gt;string&lt;/em&gt; field, it will use &lt;strong&gt;16 bytes&lt;/strong&gt; for the pointer and the length of the string, plus additional memory for the string data itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slice&lt;/strong&gt; - the slice type in Golang is also a reference type, which means that it does not have a fixed size and can be used to store any slice value. Similar to the string type, the slice type in Golang is implemented as a struct with three fields: a pointer to the slice data, the length of the slice, and the capacity of the slice.&lt;/p&gt;

&lt;p&gt;The same goes for the &lt;em&gt;map&lt;/em&gt; type in Golang and others. I avoided them here on purpose because it is hard to say how much memory they will use in this educational article. It will depend on the implementation and the data you put in them, and it opens doors to more optimizations and considerations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some tips and considerations.
&lt;/h2&gt;

&lt;p&gt;Now, when you know how to optimize your structures in Golang, you probably wonder why the compiler does not do it for you &lt;strong&gt;automatically&lt;/strong&gt;? Don't we have a compiler flag for that? The answer is quite simple: the compiler does not know &lt;strong&gt;the context&lt;/strong&gt; of your application and the logic behind your structures. You can use your structures for serialization and deserialization, for database operations, for network communication, etc. The compiler cannot know if the &lt;strong&gt;order of the fields&lt;/strong&gt; in your structure is &lt;strong&gt;important&lt;/strong&gt; for your application logic.&lt;/p&gt;

&lt;p&gt;At this time, I need to stop you to  &lt;strong&gt;think&lt;/strong&gt;. Should you really optimize your structures? In most cases, the answer is &lt;strong&gt;no&lt;/strong&gt;. The performance gain will be more visible only in certain cases, like working with large datasets, parallel batch processing with workflows, or high-performance applications. If you are working on a simple HTTP router - most likely you don't notice any difference in performance.&lt;/p&gt;

&lt;p&gt;But let's say you want to make a habit of optimizing your structures for some reason. The rule of thumb here is to group the fields by their size, moving the largest fields to the top of the structure and the smallest fields to the bottom. This might be enough for most cases. You can also use the help of the &lt;code&gt;govet&lt;/code&gt; linter in &lt;a href="https://golangci-lint.run/usage/linters/#govet" rel="noopener noreferrer"&gt;golangci-lint&lt;/a&gt; tool that you probably already use in your project. Just enable &lt;code&gt;fieldalignment&lt;/code&gt; option for &lt;code&gt;govet&lt;/code&gt; in your &lt;code&gt;.golangci.yml&lt;/code&gt; configuration file. But it might be overkill.&lt;/p&gt;

&lt;p&gt;You can also call this linter manually, since it is using a tool from the Golang standard library under the hood:&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;golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
fieldalignment &lt;span class="nt"&gt;-fix&lt;/span&gt; main.go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope this article inspired you to take a look at your code and find some places where you can benefit from padding optimizations. If you are working with large datasets - &lt;strong&gt;every byte counts&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>optimization</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Cuckoo and Bloom filters: probabilistic efficient caching.</title>
      <dc:creator>Sam Gozman</dc:creator>
      <pubDate>Sun, 25 May 2025 08:16:15 +0000</pubDate>
      <link>https://dev.to/samgozman/cuckoo-and-bloom-filters-probabilistic-efficient-caching-1jge</link>
      <guid>https://dev.to/samgozman/cuckoo-and-bloom-filters-probabilistic-efficient-caching-1jge</guid>
      <description>&lt;p&gt;In this article, I wanted to tell you about a highly efficient cache design using &lt;strong&gt;probabilistic data structures&lt;/strong&gt;.&lt;br&gt;
This technique is used in many modern applications, including web browsers, databases, and distributed systems.&lt;/p&gt;

&lt;p&gt;Cuckoo and Bloom filters are two popular types of probabilistic data structures that provide very space-efficient ways to store and lookup data that answers whether an element is a member of a set or not. The idea is to use &lt;strong&gt;optimized hashing&lt;/strong&gt; for the elements instead of storing the elements themselves.&lt;/p&gt;

&lt;p&gt;Those data structures are probabilistic in their nature, meaning that they can &lt;strong&gt;return false positives&lt;/strong&gt; but &lt;strong&gt;never false negatives&lt;/strong&gt;. Even though both algorithms allow less than 1% chance of false positives, this limits the algorithm's usefulness in many use cases. This is the price to pay for significant space efficiency and high lookup speeds.&lt;/p&gt;

&lt;p&gt;The Cuckoo filter has one big advantage over the Bloom filter: it allows &lt;strong&gt;deletion of elements&lt;/strong&gt; from the set, which can be very useful in numerous instances. It also provides better lookup performance and better space efficiency for applications requiring low false positive rates (&amp;lt; 3%). But before we dive into the details of the algorithms, let's take a look at some use cases where they can be used.&lt;/p&gt;
&lt;h2&gt;
  
  
  Use cases:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Financial fraud&lt;/strong&gt; detection. To check whether a recipient was already flagged or not. Or to check whether his card was marked as stolen or not. An additional benefit of using a probabilistic filter in this case is that financial organizations can exchange their lists of stolen or blocked credit card numbers without revealing the numbers themselves. Many other security-related checks can be cached by this algorithm.&lt;/li&gt;
&lt;li&gt;Check if &lt;strong&gt;username is taken&lt;/strong&gt; or not. Checks where username/email/domain/etc. is already taken or not.&lt;/li&gt;
&lt;li&gt;Handling a large amount of &lt;strong&gt;data requests&lt;/strong&gt; that return "not found" errors. To check if data is not found before checking in a database or 3rd party service. Imagine you have an API that returns "not found" errors for 75% of requests. This is where probabilistic filters shine because they don't produce false negatives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ad campaigns&lt;/strong&gt;. To show an ad based on if the user has signed up for a campaign or not. This is a good use case for the Cuckoo filter because it allows to delete users from the campaign if they unsubscribe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firewall network security&lt;/strong&gt;, DDoS protection systems, etc. to efficiently check whether an IP address is already blocked or not.&lt;/li&gt;
&lt;li&gt;Referee codes, discount/promo &lt;strong&gt;coupons&lt;/strong&gt;. To check if a user has already used a code or not or if a code is valid or not. This is a good use case for the Cuckoo filter because it allows to delete codes from the set if they are used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDNs and caching&lt;/strong&gt;. To optimize storage and request efficiency. For example, using Bloom filters to prevent caching of resources that are requested only once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blockchain&lt;/strong&gt; events / logs searches. Ethereum is known to use Bloom filters to optimize the search for relevant logs in a block.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those algorithms are mainly used with large data sets. The key takeaway is that you can use them to answer whether an element is a member of a set only for the cases where you can afford to have &lt;strong&gt;~1% false positives&lt;/strong&gt;. But you can use them to answer if an element is not a member of a set with 100% accuracy if you will be able to sync and maintain this set properly.&lt;/p&gt;

&lt;p&gt;So if you need to have 100% accuracy in answering whether an element is a member of a set, you should use regular hash tables or sets instead. But you will pay the price of space and time complexity for that.&lt;/p&gt;

&lt;p&gt;Choosing the right algorithm or tool is always a trade-off.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Bloom filter.
&lt;/h2&gt;

&lt;p&gt;A Bloom filter is a probabilistic data structure that is used to check if an element &lt;strong&gt;might exist&lt;/strong&gt; in a set or &lt;strong&gt;definitely not&lt;/strong&gt;. So it can guarantee only the absence of an element in the set but not its presence. Even though the false positive rate can be drastically reduced by increasing the number of hash functions, it can never be eliminated completely. However, Bloom filters do not permit deletion of items from the set.&lt;/p&gt;

&lt;p&gt;When an item is passed through a fast hashing function, bits from the hash are sampled and set from 0 to 1 at specific intervals in a &lt;strong&gt;bitfield&lt;/strong&gt;. This is how bloom filters operate. The same bits are sampled to see if they are present in a Bloom filter. Since a relational hash function generates unique identifiers, even though many items may have &lt;strong&gt;bits that overlap&lt;/strong&gt;, we can be certain that a single bit from the hash hasn't been added before if it remains a 0.&lt;/p&gt;

&lt;p&gt;A Bloom filter is an array of many bits. When an element is &lt;em&gt;added&lt;/em&gt; to a bloom filter, the element is hashed (corresponding bits are set to 1). To check whether an item is present or not, the hash is computed and the filter sees whether the corresponding bit is set or not. Of course, this is subject to collisions. If a &lt;strong&gt;collision&lt;/strong&gt; occurs, the filter will return a false positive.&lt;/p&gt;

&lt;p&gt;To reduce the risk of collisions, an entry may &lt;strong&gt;use more than one bit&lt;/strong&gt; per element. Generally, the more bits per element, the lower the likelihood of false positives. Another value affecting the accuracy of a Bloom filter is its fill ratio, or how many bits in the filter are actually set. The more bits that are set, the higher the chance of a false positive.&lt;/p&gt;

&lt;p&gt;Choosing the right number of bits per element and the target fill ratio will be determined case by case. It depends on the data size, data added rate, acceptable false positive rate, size of filter limitation, etc. The internet is full of formulas to calculate the optimal configuration for the Bloom filter. I want to talk more about practical implementations!&lt;/p&gt;
&lt;h3&gt;
  
  
  Bloom filter usage in Go.
&lt;/h3&gt;

&lt;p&gt;You would expect me here to show you a full implementation of a Bloom filter from scratch, but it rarely makes sense for production &lt;strong&gt;web applications&lt;/strong&gt;. Most likely, you would like to use it with Redis or some Redis-like solution that already has it implemented and optimized for you.&lt;/p&gt;

&lt;p&gt;Redis has a built-in support for Bloom filters. It was implemented as a separate module called &lt;code&gt;RedisBloom&lt;/code&gt; and it is available on &lt;a href="https://github.com/RedisBloom/RedisBloom" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; for Redis versions below 7. Starting from &lt;strong&gt;Redis 8 (May 2025)&lt;/strong&gt;, it is included in the core Redis distribution (not only the Bloom filter but a vast collection of probabilistic data structures as well).&lt;/p&gt;

&lt;p&gt;This is a code example of how to use Redis Bloom filters in Golang:&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;// Creates an empty Bloom filter with a single sub-filter for the initial specified capacity(1000) and with an upper bound error_rate(0.01).&lt;/span&gt;
&lt;span class="n"&gt;res1&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;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BFReserve&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="s"&gt;"bikes:models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.01&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="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="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="nb"&gt;panic&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="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;res1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; OK&lt;/span&gt;

&lt;span class="n"&gt;res2&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;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BFAdd&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="s"&gt;"bikes:models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Smoky Mountain Striker"&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="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="nb"&gt;panic&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="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;res2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; true&lt;/span&gt;

&lt;span class="n"&gt;res3&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;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BFExists&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="s"&gt;"bikes:models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Smoky Mountain Striker"&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="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="nb"&gt;panic&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="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;res3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example is taken from official Redis documentation: &lt;a href="https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/#example" rel="noopener noreferrer"&gt;https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/&lt;/a&gt;. With functions like &lt;code&gt;BFReserveExpansion&lt;/code&gt;, &lt;code&gt;BFReserveNonScaling&lt;/code&gt; and &lt;code&gt;BFReserveWithArgs&lt;/code&gt; you can control not only the initial capacity and error rate but also the scaling behavior of the filter.&lt;/p&gt;

&lt;p&gt;The required number of bits per item, given the desired error_rate and the optimal number of hash functions, is &lt;code&gt;-ln(error_rate) / ln(2)^2&lt;/code&gt;. Hence, the required number of bits in the filter is &lt;code&gt;capacity * -ln(error_rate) / ln(2)^2&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1%&lt;/strong&gt; error rate requires &lt;strong&gt;7&lt;/strong&gt; hash functions and &lt;strong&gt;9.585&lt;/strong&gt; bits per item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.1%&lt;/strong&gt; error rate requires &lt;strong&gt;10&lt;/strong&gt; hash functions and &lt;strong&gt;14.378&lt;/strong&gt; bits per item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0.01%&lt;/strong&gt; error rate requires &lt;strong&gt;14&lt;/strong&gt; hash functions and &lt;strong&gt;19.170&lt;/strong&gt; bits per item.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the total size of the filter with 1000 items and 0.01% error rate is &lt;code&gt;1000 * 19.170 = 19170 bits = 2396 bytes = 2.4 kilobytes&lt;/code&gt;. And the total size of the filter with 1000 items and 1% error rate is &lt;code&gt;1000 * 9.585 = 9585 bits = 1198 bytes = 1.2 kilobytes&lt;/code&gt;. Only requires twice more space for 0.01% vs 1% false positive rate difference.&lt;/p&gt;

&lt;p&gt;Once again, the key tradeoff is between the size of the filter and the false positive rate. Depending on your data set size, available memory, and acceptable false positive rate, you can choose the right configuration for your Bloom filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cuckoo filter.
&lt;/h2&gt;

&lt;p&gt;The Cuckoo filter is a more advanced probabilistic data structure used for high-speed set membership testing (like the Bloom filter). The main advantage over the Bloom filter is that Cuckoo &lt;strong&gt;allows deletion&lt;/strong&gt; of items from the set, which is not possible with the Bloom filter. The Cuckoo filter also provides better lookup performance and space efficiency compared to the Bloom filter.&lt;/p&gt;

&lt;p&gt;I must mention that my analysis of the Cuckoo filter is primarily based on the original research paper from 2014: &lt;a href="https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf" rel="noopener noreferrer"&gt;Cuckoo Filter: Practically Better Than Bloom&lt;/a&gt;. I recommend you read it as well if you want to understand the algorithm in more detail and check its performance against various Bloom filter variations.&lt;/p&gt;

&lt;p&gt;The Cuckoo filter is an array of &lt;strong&gt;buckets&lt;/strong&gt;, storing fingerprints of the values in one of the buckets at positions decided by the two hash functions. A membership query for item &lt;em&gt;x&lt;/em&gt; looks for &lt;em&gt;x&lt;/em&gt;'s fingerprint in all potential buckets and returns true if an identical fingerprint is found. The size of a cuckoo filter's fingerprint directly affects the false positive rate.&lt;/p&gt;

&lt;p&gt;Cuckoo filter behavior suggests that the fill rate will probably &lt;strong&gt;never reach 100%&lt;/strong&gt; since the filter is likely to declare itself full before capacity is reached. The filter will automatically create more sub-filters when it &lt;strong&gt;declares itself full&lt;/strong&gt;, resulting in decreased performance and an increased error rate. The size of the previous sub-filter is multiplied by &lt;em&gt;expansion&lt;/em&gt; rate to create the new sub-filter. The error rate increases linearly with the number of sub-filters, just like bucket size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cuckoo filter usage in Go.
&lt;/h3&gt;

&lt;p&gt;Yet again, I would like to provide you with the &lt;strong&gt;real usage example&lt;/strong&gt;, not the full implementation of the algorithm. The Cuckoo filter is also available in Redis in the same &lt;code&gt;RedisBloom&lt;/code&gt; module, which is part of &lt;strong&gt;Redis 8&lt;/strong&gt; core distribution.&lt;/p&gt;

&lt;p&gt;The usage is similar to the Bloom filter, but I would recommend using &lt;code&gt;CFReserveWithArgs&lt;/code&gt; at all times. Choosing the right bucket size, max iterations, and expansion multiplier is crucial for the performance of the Cuckoo filter. It is a more complex filter to set up than Bloom if you want to achieve &lt;strong&gt;performance gains&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;res1&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;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CFReserveWithArgs&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="s"&gt;"bikes:models"&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;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CFReserveOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;// The filter will not fill up to 100%, make sure to reserve extra capacity if you want to avoid expansions.&lt;/span&gt;
  &lt;span class="n"&gt;Capacity&lt;/span&gt;&lt;span class="o"&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;// The minimal false positive error rate is 2/255 ≈ 0.78% when bucket size of 1 is used.&lt;/span&gt;
  &lt;span class="n"&gt;BucketSize&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="c"&gt;// Number of attempts to swap items between buckets before declaring filter as full and creating an additional filter.&lt;/span&gt;
  &lt;span class="n"&gt;MaxIterations&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="c"&gt;// When a new filter is created, its size is the size of the current filter multiplied by expansion.&lt;/span&gt;
  &lt;span class="n"&gt;Expansion&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="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="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="nb"&gt;panic&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="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;res1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; OK&lt;/span&gt;

&lt;span class="n"&gt;res2&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;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CFAdd&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="s"&gt;"bikes:models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Smoky Mountain Striker"&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="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="nb"&gt;panic&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="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;res2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; true&lt;/span&gt;

&lt;span class="n"&gt;res3&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;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CFExists&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="s"&gt;"bikes:models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Smoky Mountain Striker"&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="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="nb"&gt;panic&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="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;res3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; true&lt;/span&gt;

&lt;span class="n"&gt;res4&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;rdb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CFDel&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="s"&gt;"bikes:models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Smoky Mountain Striker"&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="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="nb"&gt;panic&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="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;res4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// &amp;gt;&amp;gt;&amp;gt; true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example is mostly taken from official Redis documentation: &lt;a href="https://redis.io/docs/latest/develop/data-types/probabilistic/cuckoo-filter/#examples" rel="noopener noreferrer"&gt;https://redis.io/docs/latest/develop/data-types/probabilistic/cuckoo-filter/&lt;/a&gt;. The &lt;code&gt;CFReserveWithArgs&lt;/code&gt; function allows you to specify the bucket size, max iterations, and expansion multiplier, which are crucial for the performance of the Cuckoo filter.&lt;/p&gt;

&lt;p&gt;A wholly different approach in filter setup makes it harder to compare with the Bloom filter directly. Which is why I once again refer you to the original paper for more details on the algorithm and its linear performance change based on the number of sub-filters and bucket size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache fill control.
&lt;/h2&gt;

&lt;p&gt;When using probabilistic filters, it is important to understand that they are not a replacement for a regular cache. They are a &lt;strong&gt;complementary tool&lt;/strong&gt; that can be used to improve the performance of a cache or other data sources.&lt;/p&gt;

&lt;p&gt;Since those filters will not fill themselves with hashes, we need to talk about it. In this part, I want to list popular cache fill patterns that you might use.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write-Through Pattern.&lt;/strong&gt; In this pattern, all write operations to the data source are first written to the cache (and filters), and then to the data source. Adds overhead for write, update, and delete operations but ensures that the cache is always in sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-Memory Pattern.&lt;/strong&gt; Load all data hashes into the filter once (e.g., at application startup, with CRON jobs, workflows, etc.) and keep it in memory. This is the simplest pattern, but it requires that all data fits into memory and can be loaded at once.&lt;/li&gt;
&lt;li&gt;
&lt;del&gt;&lt;strong&gt;Cache-aside Pattern.&lt;/strong&gt;&lt;/del&gt; This is the most common caching pattern, where the application code is responsible for checking the cache first and then fetching the data from the data source if it is not found in the cache. This pattern is almost &lt;strong&gt;useless for probabilistic filters&lt;/strong&gt;. I just wanted to mention it here for completeness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are more other patterns as well, but I wanted to focus your attention first on the two most popular for probabilistic cache: &lt;strong&gt;Write-Through&lt;/strong&gt; and &lt;strong&gt;In-Memory&lt;/strong&gt;. The choice of the pattern depends on the use case, data size, and performance requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion.
&lt;/h2&gt;

&lt;p&gt;While Cuckoo filters and Bloom filters are both used in common applications, they have different strengths and weaknesses. &lt;strong&gt;Bloom&lt;/strong&gt; filters are simpler to implement and use, but they do not allow deletion of items from the set. &lt;strong&gt;Cuckoo&lt;/strong&gt; filters are more complex, but they provide better performance and allow deletion.&lt;/p&gt;

&lt;p&gt;In my opinion, I would rather &lt;strong&gt;use the Bloom filter over Cuckoo&lt;/strong&gt; in most cases, even now, 11 years after the Cuckoo filter paper was published. My main reasons are those:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bloom filter has gained widespread &lt;strong&gt;adoption&lt;/strong&gt; and has been supported by many libraries and tools for years now&lt;/li&gt;
&lt;li&gt;Bloom filter is significantly &lt;strong&gt;simpler&lt;/strong&gt; to implement and use compared to Cuckoo filter (if you need to implement it from scratch)&lt;/li&gt;
&lt;li&gt;Bloom filter has more &lt;strong&gt;predictable&lt;/strong&gt; false positive rates and performance characteristics. It is easier to calculate the required size of the filter and the number of hash functions needed for a given error rate&lt;/li&gt;
&lt;li&gt;Bloom filter is &lt;strong&gt;already significantly faster&lt;/strong&gt; and more space-efficient than regular hash tables or sets, which is the main goal of using it in the first place. Picking up the Cuckoo filter over the Bloom filter should be done with careful consideration and benchmarks based on real data size and access patterns&lt;/li&gt;
&lt;li&gt;Bloom filters typically exhibit better performance and &lt;strong&gt;scalability&lt;/strong&gt; when inserting items (if you need to add data frequently)&lt;/li&gt;
&lt;li&gt;Deletion feature of the Cuckoo filter might be a huge advantage in some cases, but there are some workarounds to achieve similar behavior with the Bloom filter. For example, using &lt;em&gt;Counting Bloom filter&lt;/em&gt; allows you to delete items from the set for the price of 3-4x space overhead against the classic Bloom filter. It is not currently supported by Redis out of the box, but you can implement it yourself or use a library that supports it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, in short, if you need a simple and efficient way to check if an element is a member of a set, use the &lt;strong&gt;Bloom filter&lt;/strong&gt;. If you need to delete items from the set and can afford the complexity overhead, use the &lt;strong&gt;Cuckoo filter&lt;/strong&gt;. If your main goal is to use the most efficient algorithm, still start with the Bloom filter. You will know in practice if you need to switch to the Cuckoo filter later on based on your data size and access patterns.&lt;/p&gt;

&lt;p&gt;Probabilistic data structures proved to be very useful and efficient over the years, and they are widely used in applications working with &lt;strong&gt;big data&lt;/strong&gt; sets or high &lt;strong&gt;RPS&lt;/strong&gt; requirements. They are a great tool to have in your toolbox, but I have to emphasize one thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never engage in premature optimizations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you implement something new, start small. &lt;strong&gt;Make it happen first;&lt;/strong&gt; you can always optimize it later. Use hash tables or sets to cache some steps, and then if you see that you have a problem with space or performance of your cache, consider using Bloom filters or Cuckoo filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cuckoo filter paper: &lt;a href="https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf" rel="noopener noreferrer"&gt;https://www.cs.cmu.edu/~dga/papers/cuckoo-conext2014.pdf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Redis Bloom official documentation: &lt;a href="https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/" rel="noopener noreferrer"&gt;https://redis.io/docs/latest/develop/data-types/probabilistic/bloom-filter/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Redis Cuckoo official documentation: &lt;a href="https://redis.io/docs/latest/develop/data-types/probabilistic/cuckoo-filter/" rel="noopener noreferrer"&gt;https://redis.io/docs/latest/develop/data-types/probabilistic/cuckoo-filter/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Bloom Filter Python implementation in great details: &lt;a href="https://www.geeksforgeeks.org/bloom-filters-introduction-and-python-implementation/" rel="noopener noreferrer"&gt;https://www.geeksforgeeks.org/bloom-filters-introduction-and-python-implementation/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>optimization</category>
      <category>go</category>
      <category>probabilistic</category>
      <category>caching</category>
    </item>
    <item>
      <title>Optimization Odyssey: pprof-ing &amp; Benchmarking Golang App</title>
      <dc:creator>Sam Gozman</dc:creator>
      <pubDate>Mon, 06 May 2024 07:39:58 +0000</pubDate>
      <link>https://dev.to/samgozman/optimization-odyssey-pprof-ing-benchmarking-golang-app-2o93</link>
      <guid>https://dev.to/samgozman/optimization-odyssey-pprof-ing-benchmarking-golang-app-2o93</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxsvlwta4n9pzr6fn3i7b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxsvlwta4n9pzr6fn3i7b.png" alt="Optimization Odyssey: pprof-ing &amp;amp; Benchmarking Golang App by gozman.space" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this post, I want to guide you through the whole process of profiling and benchmarking the Golang app. I will create a simple server app with the most naive implementation of it and will show you the step-by-step process of finding slow spots in your application with the help of the &lt;code&gt;pprof&lt;/code&gt; profiling tool.&lt;/p&gt;

&lt;p&gt;But before we get started, I want to emphasize something for you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never engage in premature optimizations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Usually, the naive approach to the problem is the best and safest. In the real world, outside Leetcode, your employer wants you to ship fast, not spending much time on optimizing nanosecond executions. Yes, there are some optimizations you still need to perform (like solving N+1 mysteries), but they are most likely to be business logic optimizations not covered in this post.&lt;/p&gt;

&lt;p&gt;Do fine-tune optimizations and benchmarking only when necessary or if you expect to see some massive load on this part of the code. Your 0.1 RPS app does not need this, believe me 🙂&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1: The Naive Approach.
&lt;/h2&gt;

&lt;p&gt;OK, so before we deep dive into the pprof profiling, let's create our elementary project first. I will create some server app with just one route that will iterate over the large JSON array to return some summary. The dummy data array will consist of some fictional data objects of users' transactions and bank accounts; the app will need to iterate over the array and sum up those numbers. For the server, I will choose &lt;code&gt;gin&lt;/code&gt; since it is the most popular tool currently, and for calculating money amounts I will choose &lt;code&gt;shopspring/decimal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everything I mentioned here in this article is available for you in the GitHub repository: &lt;a href="https://github.com/samgozman/golang-optimization-stages" rel="noopener noreferrer"&gt;samgozman/golang-optimization-stages&lt;/a&gt;. At this point, you can safely skip Stage 1 and go directly to the next chapter.&lt;/p&gt;

&lt;p&gt;The main function will be &lt;code&gt;ServeApp&lt;/code&gt; and you can find the source code for it here: &lt;a href="https://github.com/samgozman/golang-optimization-stages/blob/main/stage1/main.go" rel="noopener noreferrer"&gt;samgozman/golang-optimization-stages/blob/main/stage1/main.go&lt;/a&gt;. Nothing special about it, just a classic gin server with context shutdown. Let's keep attention on the part that will do some work, in our case the router handler &lt;code&gt;GetJSONHandler&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;// GetJSONHandler is a simple handler that returns a JSON response with a message&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;GetJSONHandler&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;gin&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;// 1. Read the content of the file dummy_data.json&lt;/span&gt;
  &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"../object/dummy_data.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;// 2. Return a JSON response with a message&lt;/span&gt;
  &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ParseJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;// 3. Calculate the total balances&lt;/span&gt;
  &lt;span class="n"&gt;currents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pendings&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;BalancesTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;// 4. Calculate the total transactions&lt;/span&gt;
  &lt;span class="n"&gt;transactionsSum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transactionsCount&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;TransactionsTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&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;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"current"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;currents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s"&gt;"pending"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;pendings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s"&gt;"transactions_sum"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;transactionsSum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s"&gt;"transactions_count"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;transactionsCount&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;It consists of 4 major functions: ReadFile, ParseJSON, BalancesTotals and TransactionsTotals.&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;// ReadFile reads a file and returns its content as a string&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;content&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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="nb"&gt;panic&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;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to read file: %w"&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;content&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;ParseJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&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;users&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unmarshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;users&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="nb"&gt;panic&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;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to unmarshal JSON: %w"&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;users&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// BalancesTotals calculates the total balances of the users.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;BalancesTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currents&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pendings&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decimal&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&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;users&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;currents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currents&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;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Balance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pendings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pendings&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;pending&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="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// TransactionsTotals calculates the total transactions of the users.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TransactionsTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decimal&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="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transactionsSum&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;
  &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transactionsCount&lt;/span&gt; &lt;span class="kt"&gt;int&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;user&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;users&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;transaction&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;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Transactions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;transactionsSum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transactionsSum&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;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;transactionsCount&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;return&lt;/span&gt; &lt;span class="n"&gt;transactionsSum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transactionsCount&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doesn't really matter what fields are in object &lt;code&gt;object.User&lt;/code&gt; for this purpose. The main thing you should know is that it is a big structure and the array size is ~1.8 MB on the disk.&lt;/p&gt;

&lt;p&gt;I think that you can find already just a few spots that could be done more efficiently, like using a pointer to the structure, combining &lt;code&gt;BalancesTotals&lt;/code&gt; and &lt;code&gt;TransactionsTotals&lt;/code&gt;, reducing the Big O complexity (nested for-loops), or counting transactions with &lt;code&gt;len()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It would be fantastic if you could spot those things. However, it is not the best approach for an optimization process because you are not sure which changes will have the biggest impact. And do they have a positive impact at all? Will this be enough?&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Im'pprof' our code: the guide into Golang profiling tool.
&lt;/h3&gt;

&lt;p&gt;To start the optimization process, we first need to prioritize our targets. To do so, we need to find the slowest parts of our application and the most memory-inefficient parts of it. This where the &lt;code&gt;pprof&lt;/code&gt; tool comes in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pprof&lt;/code&gt;, or performance profiler, is a tool that helps you to collect CPU profiles, traces, and memory heap profiles of your Golang application. It also comes with features to analyze and visualize generated &lt;code&gt;pprof&lt;/code&gt; reports (but this will require &lt;a href="http://www.graphviz.org/" rel="noopener noreferrer"&gt;graphviz&lt;/a&gt; installed).&lt;/p&gt;

&lt;p&gt;There are a few techniques of how you can capture profiles from your app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;By manually calling the &lt;code&gt;pprof&lt;/code&gt; tool in your code (in the benchmarking process)&lt;/li&gt;
&lt;li&gt;By running &lt;code&gt;pprof&lt;/code&gt; API router alongside of your application, which is just &lt;strong&gt;one&lt;/strong&gt; line of code with the help of standard library &lt;code&gt;net/http/pprof&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I will go with the first approach for this article because it can show you more of how you can use this tool. And you will not always be able to set up a &lt;code&gt;pprof&lt;/code&gt; API comfortably in your application (if you are building some desktop application or a CLI tool for instance).&lt;/p&gt;

&lt;p&gt;Now, back to the code! We need to create some very basic tests for the app, but in the case of profiling, I would recommend going with the benchmark test type.&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;BenchmarkServeApp&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="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&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="c"&gt;// Start pprof profiling&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;utils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartPprof&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;Fatal&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="c"&gt;// Create a context&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;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;WithCancel&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="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;// Run the server&lt;/span&gt;
  &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;ServeApp&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="c"&gt;// Run the benchmark&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;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;N&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;_&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;http&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="s"&gt;"http://localhost:8080/json"&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="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&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;// Cancel the context&lt;/span&gt;
  &lt;span class="n"&gt;cancel&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;I have moved &lt;code&gt;pprof&lt;/code&gt; initialization to the &lt;code&gt;utils.StartPprof&lt;/code&gt;, we will return to it in a bit. Normally, we would have to create a benchmark just for the router handler function &lt;code&gt;GetJSONHandler&lt;/code&gt;,  since it holds all the logic we are trying to optimize. But, for the educational approach, I will test the whole &lt;code&gt;ServeApp&lt;/code&gt; - because you might want to replace &lt;code&gt;gin&lt;/code&gt; router with something else to see if it improves any numbers.&lt;/p&gt;

&lt;p&gt;To run the benchmark in Go, all you have to do, besides creating a test with &lt;code&gt;b *testing.B&lt;/code&gt; as a parameter, is to run a console command:&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;test&lt;/span&gt; &lt;span class="nt"&gt;-bench&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As simple as that, but we will add some more parameters to it later on. Back to the profiling initialization function that I moved to have fewer repetitions between the steps in the repository for that post.&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;cpuprofile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"cpuprofile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"write cpu profile to `file`"&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;memprofile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"memprofile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"write memory profile to `file`"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// StartPprof starts pprof profiling&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;StartPprof&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;flag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&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;cpuprofile&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cpuprofile&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not create CPU profile: %w"&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;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;pprof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartCPUProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not start CPU profile: %w"&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="n"&gt;pprof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StopCPUProfile&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;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not close CPU profile file: %w"&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;memprofile&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;memprofile&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not create memory profile: %w"&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="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// get up-to-date statistics&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;pprof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeapProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not write memory profile: %w"&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;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;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"could not close memory profile file: %w"&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="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;Ok, that's quite a lot of code! Remember, you can avoid writing this manually if you can "afford" to run a &lt;strong&gt;profiling router&lt;/strong&gt; alongside your application. We are doing it this way not because we want just to optimize the app, but also to measure the results with controlled benchmarks.&lt;/p&gt;

&lt;p&gt;Let's take a look at this code. First, we are getting the &lt;code&gt;pprof&lt;/code&gt; options from the CLI. In the &lt;code&gt;StartPprof&lt;/code&gt; we are creating 2 separate files for binary output data from &lt;code&gt;pprof&lt;/code&gt; (btw, it is not just binary, it is basically a compiled Protobufs). In this case, we are trying to capture CPU and memory heap profiles. Pprof has a few more options you can trace, but those are the most common and important in my opinion to start with.&lt;/p&gt;

&lt;p&gt;Remember that Golang has a garbage collector? Garbage collection is the process of finding and reclaiming memory that is no longer in use by your program. In Go, this is typically managed automatically, but here we are calling &lt;code&gt;runtime.GC()&lt;/code&gt;, which is not the thing you will normally do in your application. The reason &lt;code&gt;runtime.GC()&lt;/code&gt; is called before writing the memory profile is to ensure that the memory statistics are up-to-date. If there is any memory that can be reclaimed, &lt;code&gt;runtime.GC()&lt;/code&gt; will do so, and then the memory profile will reflect the current state of memory usage after that cleanup.&lt;/p&gt;

&lt;p&gt;In contrast, CPU profiling is about tracking where time is spent, rather than resources that are used. It records the function call stack at regular intervals during the period of profiling. This is why &lt;code&gt;runtime.GC()&lt;/code&gt; is not called in the CPU profiling section - it's not relevant to the data being collected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capturing &amp;amp; reading performance profiles with pprof.
&lt;/h3&gt;

&lt;p&gt;Everything is set up and ready for the first test! Let's proceed with the actual bech &amp;amp; profiling:&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;test&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-cpuprofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cpu.pprof &lt;span class="nt"&gt;-memprofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mem.pprof &lt;span class="nt"&gt;-benchmem&lt;/span&gt; &lt;span class="nt"&gt;-bench&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-benchtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1s &lt;span class="nt"&gt;-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does is running our benchmark test with performance profile writing into &lt;code&gt;mem.pprof&lt;/code&gt; and &lt;code&gt;cpu.pprof&lt;/code&gt; files. The part I want you to keep attention to is  those 2 flags:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-benchtime=1s&lt;/code&gt; will ensure we will run as many iterations as possible within 1 second. This will help to get fast results regarding your function RPS capabilities. You can also lock the number of iterations of your code to run instead of a timeout by setting &lt;code&gt;5x&lt;/code&gt; as an argument (to run 5 times, for example).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-count=10&lt;/code&gt; is how many times we will repeat our benchmark. For fast 1s benchmarks, I recommend you to run at least 10 times to get some average values. Depending on many things in your system, each run will not be the same. This is why it is recommended to run benchmarks in some dedicated server environment with as few background processes as possible to get reliable results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After running this command, you will get this output (your actual numbers will vary):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BenchmarkServeApp-10   75   15359607 ns/op   5022236 B/op   63424 allocs/op
BenchmarkServeApp-10   79   15283220 ns/op   5019885 B/op   63420 allocs/op
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a lot of information and is not that readable at the first glance (I don't know why the go-team forgot to add some headers here, but anyway). You can manually average down those values or to run longer tests (also benchmark iterations can be compared automatically by the CLI tool, but I will address it later). Let's take a look at how we can read those values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BenchmarkServeApp-10&lt;/strong&gt; - The &lt;code&gt;-10&lt;/code&gt; indicates the env &lt;code&gt;GOMAXPROCS&lt;/code&gt; value, which is the maximum number of CPUs that can be executing simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;75, 79,&lt;/strong&gt; etc.: These are the number of iterations that the benchmark was able to run in the time specified by &lt;code&gt;-benchtime&lt;/code&gt; (1 second in our case).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15359607 ns/op, 15283220 ns/op,&lt;/strong&gt; etc.: This is the average time taken per operation, in nanoseconds. An "operation" is one iteration of your benchmark function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5022236 B/op, 5019885 B/op,&lt;/strong&gt; etc.: This is the average amount of memory allocated per operation, in bytes. This includes both heap and stack memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;63424 allocs/op, 63420 allocs/op,&lt;/strong&gt; etc.: This is the average number of memory allocations made per operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now that you understand how to read Golang benchmark values, it is time for the next part: reading &lt;code&gt;mem.pprof&lt;/code&gt; and &lt;code&gt;cpu.pprof&lt;/code&gt; profiles.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Small reminder - &lt;a href="http://www.graphviz.org/" rel="noopener noreferrer"&gt;graphviz&lt;/a&gt; installation required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I will start from the &lt;code&gt;mem.pprof&lt;/code&gt;, since it is impossible to open them both with one command. To do so, run this command and head into the browser:&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 &lt;span class="nt"&gt;-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;':8081'&lt;/span&gt; mem.pprof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the URL provided in the console to access &lt;code&gt;pprof&lt;/code&gt; UI. Before you will be overwhelmed with the chart, let's look at the menu. In the "Sample" section, there are 4 options available: &lt;code&gt;alloc_objects&lt;/code&gt;, &lt;code&gt;alloc_space&lt;/code&gt;, &lt;code&gt;inuse_objects&lt;/code&gt; and &lt;code&gt;inuse_space&lt;/code&gt;. I think it is obvious from the name what each sample group represents. The group that is interested the most to me is &lt;code&gt;alloc_space&lt;/code&gt; - click on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flq7jk9empdb5kwjtsrbi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flq7jk9empdb5kwjtsrbi.png" alt="pprof graph alloc_space" width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you see here is a call stack of functions used in our app and the libraries inside it. Just by looking at this graph view, you can already notice what functions allocated the most space during execution. We had only 4 functions in &lt;code&gt;GetJSONHandler&lt;/code&gt; where &lt;code&gt;ParseJSON&lt;/code&gt; with &lt;code&gt;ReadFile&lt;/code&gt; is draining most of the memory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Focus on optimizing function in the descending order of the resources they use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, in the "View" section of the menu, choose "Flame Graph" view. The default graph was great, but I prefer to use "Flame Graph" here (I found it more compact and readable).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9hunycyokq77z0skux0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe9hunycyokq77z0skux0.png" alt="pprof flame stage 1" width="799" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You have probably noticed an alarming memory usage value, almost &lt;strong&gt;4 GB&lt;/strong&gt; in our case! Oh, that's a lot! But don't be frightened, it is not a constant RAM pressure. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pprof&lt;/code&gt; tool in Go provides a visualization of the memory usage of your program. When you see large numbers like 4 GB, it's showing the cumulative size of the objects allocated in your program, not the current memory usage. And, since we were running the command with the &lt;code&gt;-count=10&lt;/code&gt; flag for 1 second each iteration, you see the cumulative size for 10 runs.&lt;/p&gt;

&lt;p&gt;The pprof tool records every allocation made by your program. If your program allocates plenty of small objects that are quickly garbage collected, the total size of those allocations can be much larger than the actual memory usage of your program at any given moment.&lt;/p&gt;

&lt;p&gt;For example, if your program allocates 1 MB of memory 4000 times, and each time it frees that memory before the next allocation, your program would never use more than 1 MB of memory at a time. However, &lt;code&gt;pprof&lt;/code&gt; would report that your program allocated 4 GB of memory.&lt;/p&gt;

&lt;p&gt;So, the large numbers you're seeing in &lt;code&gt;pprof&lt;/code&gt; are not a measure of your program's memory usage, but rather a measure of its memory allocation. It's a useful tool for identifying parts of your code that are creating a lot of garbage for the garbage collector to clean up, which can slow down your program.&lt;/p&gt;




&lt;p&gt;Ok, that was a lot of information. But we are not done, not yet! Remember that we are still having the &lt;code&gt;cpu.pprof&lt;/code&gt; to process? Stop the current &lt;code&gt;pprof&lt;/code&gt; execution and open the next file:&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 &lt;span class="nt"&gt;-http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;':8081'&lt;/span&gt; cpu.pprof
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the first seconds, you can notice that we are having a larger and more complex graph. And you can see that your application was used ~51% of the CPU time… And the rest was utilized by Golang runtime and garbage collector. Let's switch the view to "Flame Graph" to look at our program (the sample setting is "cpu"):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhh5sfhkjn2ebaqzm9dz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhh5sfhkjn2ebaqzm9dz6.png" alt="pprof flame cpu stage 1" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;See how it differs from memory usage? Most of the CPU time was utilized on the &lt;code&gt;ParseJSON&lt;/code&gt; function. Every other function that we wrote are having so insignificant an impact on the CPU, that we can't even set their names on the chart without clicking on the items. But is it a correct conclusion? No, it is not.&lt;/p&gt;

&lt;p&gt;While you can't see &lt;code&gt;ReadFile&lt;/code&gt;, &lt;code&gt;BalancesTotals&lt;/code&gt; &amp;amp; &lt;code&gt;TransactionsTotals&lt;/code&gt; functions here, they are impacting your application performance in the most hidden way: through the runtime and garbage collector (GC). Our root application only occupies about &lt;strong&gt;51&lt;/strong&gt;&lt;strong&gt;%&lt;/strong&gt; of CPU time. Every unnecessary memory allocation in nested loops, pointers misuse, redundant goroutines etc. will lead to pressure on the CPU via the runtime process and GC. It is the rest 49% of our program and there is no easy way to measure the impact here (and some processes we can't control), but we will use this information to compare the impact of our future optimizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2: Optimizing JSON parsing in Golang.
&lt;/h2&gt;

&lt;p&gt;Before we continue, there is something I need you to modify in the &lt;code&gt;go test&lt;/code&gt; command. Add at the end of it &lt;code&gt;&amp;gt; stage1.txt&lt;/code&gt; to save the output to the text file instead of the console. We will use it later to assess our results between stages.&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;test&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-cpuprofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cpu.pprof &lt;span class="nt"&gt;-memprofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mem.pprof &lt;span class="nt"&gt;-benchmem&lt;/span&gt; &lt;span class="nt"&gt;-bench&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-benchtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1s &lt;span class="nt"&gt;-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; stage1.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also will need to install &lt;code&gt;benchstat&lt;/code&gt; cmd if you would rather not compare benchmark results manually every single time &lt;a href="https://cs.opensource.google/go/x/perf" rel="noopener noreferrer"&gt;https://cs.opensource.google/go/x/perf&lt;/a&gt;.&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;golang.org/x/perf/cmd/benchstat@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From every performance metric that we've seen so far: JSON parsing is our main bottleneck. It is a well-known problem for applications that need to operate under high RPS load. Our first step here is to find a more efficient library to handle JSON tasks. I will save you some time and will go on with &lt;a href="https://github.com/goccy/go-json" rel="noopener noreferrer"&gt;go-json&lt;/a&gt;, you will only need to replace import. You can also go with &lt;code&gt;sonic&lt;/code&gt; library, but its performance is very dependent on the target system.&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="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/goccy/go-json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it's time to run our performance test for the second time, but this time we will save it into &lt;code&gt;stage2.txt&lt;/code&gt; file.&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;test&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-cpuprofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cpu.pprof &lt;span class="nt"&gt;-memprofile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mem.pprof &lt;span class="nt"&gt;-benchmem&lt;/span&gt; &lt;span class="nt"&gt;-bench&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-benchtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1s &lt;span class="nt"&gt;-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; stage2.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just by looking at the &lt;code&gt;stage2.txt&lt;/code&gt; output, I can already tell that we made some huge difference in terms of performance. The number of iterations we were able to perform in one second significantly increased by &lt;strong&gt;3-3.5x&lt;/strong&gt; times!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BenchmarkServeApp-10   229  4959000 ns/op  6174943 B/op   31765 allocs/op
BenchmarkServeApp-10   247  4957436 ns/op  6168185 B/op   31759 allocs/op
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is time to compare results with our new tool. To run the &lt;code&gt;benchstat&lt;/code&gt; we only need to provide those files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;benchstat &lt;span class="nt"&gt;-table&lt;/span&gt; .fullname ./stage1/stage1.txt ./stage2/stage2.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let's look at the results&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;benchstat &lt;span class="nt"&gt;-table&lt;/span&gt; .fullname ./stage1/stage1.txt ./stage2/stage2.txt
.fullname: ServeApp-10
            │ ./stage1/stage1.txt │         ./stage2/stage2.txt         │
            │       sec/op        │   sec/op     vs base                │
ServeApp-10          15.569m ± 2%   5.221m ± 5%  &lt;span class="nt"&gt;-66&lt;/span&gt;.47% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage1/stage1.txt │         ./stage2/stage2.txt          │
            │        B/op         │     B/op      vs base                │
ServeApp-10          4.785Mi ± 0%   5.732Mi ± 3%  +19.77% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage1/stage1.txt │         ./stage2/stage2.txt         │
            │      allocs/op      │  allocs/op   vs base                │
ServeApp-10           63.42k ± 0%   31.75k ± 0%  &lt;span class="nt"&gt;-49&lt;/span&gt;.94% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benchmark results show that after replacing the standard JSON decoder with &lt;code&gt;go-json&lt;/code&gt;, the execution time per operation (sec/op) decreased by approximately 66.47%, indicating a significant improvement in performance. However, the memory usage per operation (B/op) increased by about 19.77%, and the number of allocations per operation (allocs/op) decreased by nearly 49.94%, suggesting that while the operation became faster, it also became slightly more memory-intensive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjes15v5l20rcb5likhgr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjes15v5l20rcb5likhgr.png" alt="pprof flame stage 2" width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After looking at the updated pprof flame graph, you can see that &lt;code&gt;ParseJSON&lt;/code&gt; function still takes up a lot of space here. You can also notice that the total cumulative memory usage skyrocketed from 4 GB to almost 20 GB! But keep in mind, that we were able to increase the number of operations per second by roughly 3 times and, well, B/op is up 19.77%, which was a tradeoff. This explains such a difference in cumulative memory usage.&lt;/p&gt;

&lt;p&gt;Overall, this was a giant step in our optimization journey! Let's continue forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 3: Optimizing ReadFile operation.
&lt;/h2&gt;

&lt;p&gt;Our next step is to increase &lt;code&gt;ReadFile&lt;/code&gt; performance. Instead of relying on &lt;code&gt;os.ReadFile&lt;/code&gt; we can use the built-in &lt;code&gt;bufio&lt;/code&gt; package. Buffio implements a buffered reader and writer of the same interface.&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;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&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="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;file&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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="nb"&gt;panic&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;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open file: %w"&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;bufio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&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="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&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;ParseJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&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;users&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&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;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&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;users&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="nb"&gt;panic&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;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to unmarshal JSON: %w"&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;users&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;GetJSONHandler&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;gin&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="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;data&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;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"../object/dummy_data.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ParseJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c"&gt;// Close the file reader after reading the content.&lt;/span&gt;
  &lt;span class="c"&gt;// Note: it can be deferred, but it's better to close it as soon as possible.&lt;/span&gt;
  &lt;span class="n"&gt;cancel&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary distinction between buffered I/O and standard I/O lies in how data is processed. Buffered I/O operates by reading or writing data in blocks, rather than handling one byte at a time.&lt;/p&gt;

&lt;p&gt;Let's run our tests and compare the results!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BenchmarkServeApp-10     189   5937594 ns/op 4593636 B/op   31772 allocs/op
BenchmarkServeApp-10     190   6154874 ns/op 4559697 B/op   31766 allocs/op
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see that something is not right. The number of iterations decreased (from ~250 in Stage 2 to 190). Next, I will run a &lt;code&gt;benchstat&lt;/code&gt; to compare results further:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;benchstat &lt;span class="nt"&gt;-table&lt;/span&gt; .fullname ./stage2/stage2.txt ./stage3/stage3.txt
.fullname: ServeApp-10
            │ ./stage2/stage2.txt │         ./stage3/stage3.txt         │
            │       sec/op        │   sec/op     vs base                │
ServeApp-10           4.795m ± 2%   6.155m ± 3%  +28.37% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage2/stage2.txt │         ./stage3/stage3.txt          │
            │        B/op         │     B/op      vs base                │
ServeApp-10          5.699Mi ± 2%   4.235Mi ± 3%  &lt;span class="nt"&gt;-25&lt;/span&gt;.70% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage2/stage2.txt │        ./stage3/stage3.txt         │
            │      allocs/op      │  allocs/op   vs base               │
ServeApp-10           31.75k ± 0%   31.76k ± 0%  +0.04% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.012 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The execution time per operation (sec/op) increased by 28% (this explains the significant drop in iterations), however, the memory usage per operation (B/op) decreased by almost the same amount! So we are using less memory, but need more CPU to achieve this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What we have learned here is that each optimization has its own drawbacks. You need to consider what you are trying to achieve - whether it's speeding up your application or increasing its performance. These objectives do not always go hand in hand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I will continue to focus on speeding up the application here, so we should devise a different approach suitable for our case. Since we are consistently serving only one file, and it remains intact at all times, we can cache it.&lt;/p&gt;

&lt;p&gt;The best approach here will be to use &lt;code&gt;sync.Once&lt;/code&gt; struct from the standard package. It will ensure that &lt;code&gt;ReadFile&lt;/code&gt; saves the data just once.&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="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;once&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;Once&lt;/span&gt;
  &lt;span class="n"&gt;content&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;func&lt;/span&gt; &lt;span class="n"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;once&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
        &lt;span class="n"&gt;content&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;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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="nb"&gt;panic&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;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to read file: %w"&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neat and simple. We should run our benchmarks to see the impact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BenchmarkServeApp-10     280   4208854 ns/op 4188958 B/op   31745 allocs/op
BenchmarkServeApp-10     286   4146523 ns/op 4165174 B/op   31741 allocs/op
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK, that was better, really better! The number of iterations increased from Stage 2 by about 10-12%. Let's look at the &lt;code&gt;benchstat&lt;/code&gt;  as we did before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;benchstat &lt;span class="nt"&gt;-table&lt;/span&gt; .fullname ./stage2/stage2.txt ./stage3/stage3.txt
.fullname: ServeApp-10
            │ ./stage2/stage2.txt │        ./stage3/stage3.txt         │
            │       sec/op        │   sec/op     vs base               │
ServeApp-10           4.795m ± 2%   4.329m ± 3%  &lt;span class="nt"&gt;-9&lt;/span&gt;.71% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage2/stage2.txt │         ./stage3/stage3.txt          │
            │        B/op         │     B/op      vs base                │
ServeApp-10          5.699Mi ± 2%   3.928Mi ± 2%  &lt;span class="nt"&gt;-31&lt;/span&gt;.07% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage2/stage2.txt │        ./stage3/stage3.txt         │
            │      allocs/op      │  allocs/op   vs base               │
ServeApp-10           31.75k ± 0%   31.74k ± 0%  &lt;span class="nt"&gt;-0&lt;/span&gt;.03% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After caching the file, the benchmarking results show a significant performance improvement. The operation time decreased by approximately 9.71%, the memory usage declined by around 31.07%, and the number of allocations per operation remained almost the same.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxc17p165luu1fhrh8vj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxc17p165luu1fhrh8vj.png" alt="pprof flame stage 3" width="800" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can't even find the &lt;code&gt;ReadFile&lt;/code&gt; function from the &lt;code&gt;pprof&lt;/code&gt; UI! Furthermore, good to mention that the total cumulative memory usage has noticeably reduced from 20 GB to 15 GB. I'm pleased with the results and will continue further!&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 4: Pointers &amp;amp; Loops.
&lt;/h2&gt;

&lt;p&gt;I think that the optimizations described in this chapter were the most obvious from the start. The first reason I kept them for so long is that you are most likely to use pointers automatically and can spot unnecessary loops on your own. The second reason is that while this approach matters, it will only help us just a tiny bit in comparison with the previous things.&lt;/p&gt;

&lt;p&gt;I will change 3 things in the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Replace all &lt;code&gt;[]object.User&lt;/code&gt; with &lt;code&gt;[]*object.User&lt;/code&gt;. If you are new to this, you might want to do something like &lt;code&gt;*[]&lt;/code&gt;, but it will make things worse in any use case. Copying pointers is a cheap operation.&lt;/li&gt;
&lt;li&gt;Combine &lt;code&gt;BalancesTotals&lt;/code&gt; &amp;amp; &lt;code&gt;TransactionsTotals&lt;/code&gt; into one function to remove redundant loops. Sidenote: you can also flatten the data, so you can remove a nested loop, but that will require a bit more time to cook.&lt;/li&gt;
&lt;li&gt;Count transactions with &lt;code&gt;len()&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I think that is enough of the code being shown here already. If you're interested, you can see the full example in the GitHub repository: &lt;a href="https://github.com/samgozman/golang-optimization-stages/tree/main/stage3" rel="noopener noreferrer"&gt;samgozman/golang-optimization-stages/tree/main/stage3&lt;/a&gt;. Let's move to benchmarking once again…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BenchmarkServeApp-10     277   3953330 ns/op 3697193 B/op   31744 allocs/op
BenchmarkServeApp-10     306   3929780 ns/op 3693402 B/op   31739 allocs/op
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just a slight difference, &lt;code&gt;benchstat&lt;/code&gt;  will show us a bit more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;benchstat &lt;span class="nt"&gt;-table&lt;/span&gt; .fullname ./stage3/stage3.txt ./stage4/stage4.txt
.fullname: ServeApp-10
            │ ./stage3/stage3.txt │      ./stage4/stage4.txt      │
            │       sec/op        │   sec/op     vs base          │
ServeApp-10           4.329m ± 3%   4.264m ± 7%  ~ &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.684 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage3/stage3.txt │         ./stage4/stage4.txt          │
            │        B/op         │     B/op      vs base                │
ServeApp-10          3.928Mi ± 2%   3.519Mi ± 0%  &lt;span class="nt"&gt;-10&lt;/span&gt;.42% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage3/stage3.txt │      ./stage4/stage4.txt      │
            │      allocs/op      │  allocs/op   vs base          │
ServeApp-10           31.74k ± 0%   31.73k ± 0%  ~ &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.467 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The memory usage declined by 10% from the previous iteration, everything else is the same. Most of the impact here comes from switching to pointers, the loop optimization hasn't affected the performance much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 5: Cache Cheating.
&lt;/h2&gt;

&lt;p&gt;And now it is the time for a little cheat which is only applicable to our code example! Remember that our router only serves one file every single time? It is not that uncommon, especially if your server does the job of serving front-end texts. In Stage 3 I introduced &lt;code&gt;sync.Once&lt;/code&gt; for the &lt;code&gt;ReadFile&lt;/code&gt; function. But we can use it for the whole handler logic!&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="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;once&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;Once&lt;/span&gt;
  &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H&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;GetJSONHandler&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;gin&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="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;once&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Read the content of the file dummy_data.json&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"../object/dummy_data.json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Return a JSON response with a message&lt;/span&gt;
    &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ParseJSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// 3. Calculate the total balances and transactions&lt;/span&gt;
    &lt;span class="n"&gt;currents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pendings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transactionsSum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transactionsCount&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;GetTotals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"current"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;currents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="s"&gt;"pending"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;pendings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="s"&gt;"transactions_sum"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;transactionsSum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="s"&gt;"transactions_count"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;transactionsCount&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&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;I have moved &lt;code&gt;once.Do&lt;/code&gt; from  &lt;code&gt;ReadFile&lt;/code&gt; into &lt;code&gt;GetJSONHandler&lt;/code&gt;. Now we only create this response once, which makes all our previous optimizations kinda useless. But I wanted to show them because this particular cheat can only be used in some very specific examples. In more complicated scenarios, you will probably cache responses in some Redis-like key-value storage via router middleware. Nevertheless, the global idea is the same: use cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BenchmarkServeApp-10    3146    466019 ns/op   24553 B/op     174 allocs/op
BenchmarkServeApp-10    2426    511220 ns/op   22710 B/op     171 allocs/op
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Astonishing results!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;benchstat &lt;span class="nt"&gt;-table&lt;/span&gt; .fullname ./stage4/stage4.txt ./stage5/stage5.txt
.fullname: ServeApp-10
            │ ./stage4/stage4.txt │          ./stage5/stage5.txt          │
            │       sec/op        │    sec/op      vs base                │
ServeApp-10          4264.5µ ± 7%   805.6µ ± 340%  &lt;span class="nt"&gt;-81&lt;/span&gt;.11% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage4/stage4.txt │          ./stage5/stage5.txt          │
            │        B/op         │     B/op       vs base                │
ServeApp-10        3603.50Ki ± 0%   27.12Ki ± 12%  &lt;span class="nt"&gt;-99&lt;/span&gt;.25% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;

            │ ./stage4/stage4.txt │        ./stage5/stage5.txt         │
            │      allocs/op      │ allocs/op   vs base                │
ServeApp-10          31733.5 ± 0%   174.0 ± 5%  &lt;span class="nt"&gt;-99&lt;/span&gt;.45% &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.000 &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now this router barely uses anything, it is just crazy! Yes, it is cheating, but the legal one in the case of this example. I suggest to look at our &lt;code&gt;alloc_space&lt;/code&gt; graph for the last time, just for fun:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkff8nywgd0hc8p8ir1d0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkff8nywgd0hc8p8ir1d0.png" alt="pprof flame stage 5" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is difficult to notice any function on this graph that we've been optimizing for all this time! The total cumulative memory usage is now 1/8 of its original value. The same applies to the CPU profile as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb3jiwmigps8hr8onutpv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb3jiwmigps8hr8onutpv.png" alt="pprof flame cpu stage 5" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;It was too much information for one blog post, but I'm glad I didn't split it in half! We moved from our first naive approach, which could hold up to 78 requests per second, to the optimal approach in stage 4 which helped to increase RPS up to 279 on average. And in stage 5 we've used some illegal cache cheating to move the plank even higher to up to  1487  RPS.&lt;/p&gt;

&lt;p&gt;Everything that we've covered here is about how to approach the optimization process, what steps to take, and how to use &lt;code&gt;pprof&lt;/code&gt; and benchmarks in Golang.&lt;/p&gt;

&lt;p&gt;Some tips and notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Never engage in premature optimizations&lt;/li&gt;
&lt;li&gt;Read more about &lt;code&gt;pprof&lt;/code&gt; in the README file: &lt;a href="https://github.com/google/pprof/blob/main/doc/README.md" rel="noopener noreferrer"&gt;github.com/google/pprof&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;You can optimize work with large objects by using &lt;code&gt;sync.Pool&lt;/code&gt; method. It has its drawbacks, but will help to reduce the number of allocations in some cases&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;sync.Once&lt;/code&gt; function will lead us to the usage of global variables, which is an antipattern in most languages. Use this thing wisely and never export those variables to the outside&lt;/li&gt;
&lt;li&gt;If you want to bench some part of the application inside the test, be sure not to use any mocks in it, as it will lead to some troubles: &lt;code&gt;testing.B&lt;/code&gt; interface is not expected by most mocking techniques (&lt;code&gt;testing.T&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;If your benchmark setup process takes significant time before the actual testing - use &lt;code&gt;b.ResetTimer()&lt;/code&gt; function to reset timer statistics&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;net/http/pprof&lt;/code&gt; package to run &lt;code&gt;pprof&lt;/code&gt; API alongside your application (this can help to "debug"  some problems on production)&lt;/li&gt;
&lt;li&gt;When optimizing your code, strive not to sacrifice readability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you found this information useful and see you in the next post! Subscribe for future updates :)&lt;/p&gt;

</description>
      <category>profiling</category>
      <category>pprof</category>
      <category>go</category>
      <category>optimization</category>
    </item>
    <item>
      <title>The State of Dependency Injections in Golang</title>
      <dc:creator>Sam Gozman</dc:creator>
      <pubDate>Mon, 22 Apr 2024 16:00:33 +0000</pubDate>
      <link>https://dev.to/samgozman/the-state-of-dependency-injections-in-golang-pbj</link>
      <guid>https://dev.to/samgozman/the-state-of-dependency-injections-in-golang-pbj</guid>
      <description>&lt;p&gt;Dependency injection (DI) is a well-established concept in programming. This is the design pattern in software engineering where dependencies are injected into a class rather than the class creating them itself, promoting loose coupling and easier testing.&lt;/p&gt;

&lt;p&gt;However, when it comes to Golang, where classic object-oriented programming (OOP) constructs like classes are absent (&lt;code&gt;struct&lt;/code&gt;'s don’t count, sorry mates), implementing dependency injection poses some slight differences. But you will soon see that this practice is quite easy to implement in Golang, even without third-party dependencies. In fact, I'm 100% certain that you're already using DI in your Golang applications because this pattern is almost unavoidable.&lt;/p&gt;

&lt;p&gt;Later in this article, I will demonstrate you how you can enforce your experience with DI in Go by using a simple tool from Google called &lt;code&gt;wire.&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  DI without dependencies.
&lt;/h2&gt;

&lt;p&gt;If you have ever written unit tests in Go, then you are likely already familiar with this practice. It is the only way to do a unit test when you have some database connections or any kind of third party external service used in your function. You will need to &lt;code&gt;mock&lt;/code&gt; this service. To achieve this, you will have to &lt;code&gt;provide&lt;/code&gt; it (either to the function directly, or into the &lt;code&gt;struct&lt;/code&gt; your method assigned to). You will also need to have an interface for your third-party service and both your &lt;code&gt;mock&lt;/code&gt; and your &lt;code&gt;service&lt;/code&gt; should implement the methods of an interface.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Accept interfaces, return structs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, you will have a structure like this in some &lt;code&gt;service.go&lt;/code&gt; file:&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;// Some service your App depends on&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ThirdPartyService&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;baseURL&lt;/span&gt; &lt;span class="kt"&gt;string&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;NewThirdPartyService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ThirdPartyService&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;ThirdPartyService&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;baseURL&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ThirdPartyService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FindSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resp&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;http&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="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&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="s"&gt;""&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="k"&gt;defer&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;Body&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadCloser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;body&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;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&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="s"&gt;""&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="k"&gt;return&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;body&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;type&lt;/span&gt; &lt;span class="n"&gt;ThirdPartyServiceInterface&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;FindSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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="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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;App&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;service&lt;/span&gt; &lt;span class="n"&gt;ThirdPartyServiceInterface&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;NewApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="n"&gt;ThirdPartyServiceInterface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;App&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;App&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;service&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="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="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;DoSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FindSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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 is the way how you can create a dependency injections in Golang, and it also allows you to mock your dependencies in the unit tests.&lt;/p&gt;

&lt;p&gt;To use this App in your code, you will need to construct all its dependencies first and then proceed with the NewApp.&lt;/p&gt;

&lt;p&gt;In order to unit test your &lt;code&gt;App&lt;/code&gt; you will need to mock this &lt;code&gt;ThirdPartyService&lt;/code&gt;. There are various methods to generate mocks from the interface, but I will not discuss it here and will stick a manual solution with classic &lt;a href="https://github.com/stretchr/testify" rel="noopener noreferrer"&gt;testify/mock&lt;/a&gt; and &lt;a href="https://github.com/stretchr/testify" rel="noopener noreferrer"&gt;testify/assert&lt;/a&gt; which are commonly used in many mockery libraries under the hood.&lt;/p&gt;

&lt;p&gt;Here is an example of your test with mock:&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;"testing"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/stretchr/testify/assert"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/stretchr/testify/mock"&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;MockThirdPartyService&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;mock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mock&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;MockThirdPartyService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FindSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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="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="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&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;Called&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&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;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&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="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestApp_DoSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mockThirdPartyService&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MockThirdPartyService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockThirdPartyService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mockThirdPartyService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;On&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"FindSomething"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"response"&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="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;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoError&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;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Equal&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="s"&gt;"response"&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;What we just did is created a mock for the &lt;code&gt;ThirdPartyService&lt;/code&gt; and injected it as a dependency into your app.&lt;/p&gt;

&lt;p&gt;See? You are already employing this design pattern even if you weren't consciously aware of it. There is no way you can escape from dependency injection in Go since you can’t stub and spy on your method as easy as it was in Node.js with a &lt;a href="https://sinonjs.org/" rel="noopener noreferrer"&gt;sinonjs&lt;/a&gt; library, for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  That was easy, but what is the challenge?
&lt;/h2&gt;

&lt;p&gt;The challenge comes when your app grows bigger. In fact, you are having multiple binaries using dozens of internal and external dependencies, and it becomes harder to organize and reuse your components and to maintain low coupling.&lt;/p&gt;

&lt;p&gt;Now, imagine you have a dozen &lt;code&gt;App&lt;/code&gt; structures, each represents the struct of its own binary. And each &lt;code&gt;App&lt;/code&gt; is having a unique sets of &lt;code&gt;components&lt;/code&gt;, although there may be intersections between them. And some &lt;code&gt;components&lt;/code&gt; will have their very own dependencies etc.&lt;/p&gt;

&lt;p&gt;Well, it is not a big challenge; you can manage this manually, no doubt. However, this approach will require consistency from you and is likely to be error-prone. Additionally, you'll need to monitor unused dependencies in your components and apps. A preferable approach is to enforce this practice with an external tool to mitigate common mistakes.&lt;/p&gt;

&lt;p&gt;There are many dependency injection tools on the market, for example: &lt;a href="https://github.com/samber/do" rel="noopener noreferrer"&gt;samber/do&lt;/a&gt;, or &lt;a href="https://github.com/uber-go/fx" rel="noopener noreferrer"&gt;uber/fx&lt;/a&gt;. However, they all share one common trait - it is not just about dependency injection, it is more like a DI framework. Lots of features, necessity to learn specific syntax and write your services and components in the prescribed manner. And sometimes those tools can introduce breaking changes. Furthermore, if you intend to share your components with other services, you'll need to use the same DI tool, or alternatively, write a wrapper.&lt;/p&gt;

&lt;p&gt;I want to show you the different approach (my favourite among others) - &lt;a href="https://github.com/google/wire" rel="noopener noreferrer"&gt;google/wire&lt;/a&gt;. It will be much, much simpler and will not force you to stick with some specific syntax. Your components can be shared with others without forcing them to use your tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire up with Google.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/google/wire" rel="noopener noreferrer"&gt;google/wire&lt;/a&gt; is an external tool for Golang made by Google to generate code that helps us to connect component using DI. There is no specific syntax of how you should write your &lt;code&gt;components&lt;/code&gt;. It can be a structure of any kind, like you already have. As a result, the learning curve is minimal, as you only need to write a wire.go file.&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;//go:build wireinject&lt;/span&gt;

&lt;span class="c"&gt;//go:generate go run github.com/google/wire/cmd/wire&lt;/span&gt;

&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="s"&gt;"github.com/google/wire"&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;initApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseURL&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;App&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="n"&gt;wire&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="c"&gt;// All providers needed to create the App or other providers that are needed in between&lt;/span&gt;
        &lt;span class="n"&gt;NewThirdPartyService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c"&gt;// NewApp is the function that creates the final App struct&lt;/span&gt;
        &lt;span class="n"&gt;NewApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="c"&gt;// Bind the interface to the implementation so Wire knows which one to use.&lt;/span&gt;
        &lt;span class="c"&gt;// Accept interfaces, return structs. Remember?&lt;/span&gt;
        &lt;span class="n"&gt;wire&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThirdPartyServiceInterface&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ThirdPartyService&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;App&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, that's basically it. You can run “go generate” to produce the new code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go generate ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will should generate wire_gen.go file with app/component constructor:&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;// Code generated by Wire. DO NOT EDIT.&lt;/span&gt;

&lt;span class="c"&gt;//go:generate go run -mod=mod github.com/google/wire/cmd/wire&lt;/span&gt;
&lt;span class="c"&gt;//go:build !wireinject&lt;/span&gt;
&lt;span class="c"&gt;// +build !wireinject&lt;/span&gt;

&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;

&lt;span class="c"&gt;// Injectors from wire.go:&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;initApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseURL&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="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;App&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="n"&gt;thirdPartyService&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewThirdPartyService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thirdPartyService&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;app&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does is try to match all dependencies that you provided with their independent components. One dependency can be used in different components at the same time. There are no strict ordering rules for the dependencies to be processed in &lt;code&gt;wire.Build&lt;/code&gt;. If you have redundant dependencies, the wire tool will notify you. Similarly, if you're missing dependencies from your build configuration, it will also alert you.&lt;/p&gt;

&lt;p&gt;Now, you can simply run &lt;code&gt;initApp(...)&lt;/code&gt; in your &lt;code&gt;main ()&lt;/code&gt; to access the built App. There's no need to manually create an initialization function for each dependency in every app ever again. This becomes very convenient when you have dozens of components within the same app. For instance, let's consider a more elaborate wire build:&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;wire&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ProvideLogs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideEmailService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideAuthService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideTemporalService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideAwsStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideAwsSns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideGrpcClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;NewApp&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;Logs can be used in any components, so as Config and others. Wire will connect all of them, so you don’t have to worry about unreadable &lt;code&gt;main()&lt;/code&gt; or &lt;code&gt;init()&lt;/code&gt; functions. Remember to add &lt;code&gt;wire.Bind&lt;/code&gt; to associate interfaces with implementations for Wire.&lt;/p&gt;

&lt;p&gt;Since one component can have multiple dependencies, you can organize them into &lt;code&gt;ProviderSet&lt;/code&gt; by using &lt;code&gt;wire.NewSet()&lt;/code&gt; function and storing alongside your component:&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;ProviderSet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wire&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ProvideConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProvideService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wire&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceInterface&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&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;No magic is done here, no special syntax to learn - it will just make your wire.go files look cleaner by grouping all the things in one place:&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;initApp&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;cfg&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&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="n"&gt;serverApp&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="n"&gt;wire&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;captcha&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProvideServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProviderSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

        &lt;span class="n"&gt;newServerApp&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;serverApp&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find this code example of &lt;code&gt;google/wire&lt;/code&gt; usage in my &lt;a href="https://github.com/samgozman/go-bloggy/blob/main/cmd/server/wire.go" rel="noopener noreferrer"&gt;go-bloggy&lt;/a&gt; repository. In fact, this blog is built with Wire :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up the Wire.
&lt;/h2&gt;

&lt;p&gt;In my opinion, Wire is the easiest and simplest dependency injection tool I’ve seen in my life, not only in Golang exclusively. It just gets the job done and nothing else. No learning curve, no special syntax; you can seamlessly add it to your project, without even touching the code of your existing components. This means your code and components remain highly reusable, even in repositories that don't use Wire. Love it 🧡&lt;/p&gt;

&lt;p&gt;Some tips and tricks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;em&gt;Provide&lt;/em&gt; verb instead of &lt;em&gt;New&lt;/em&gt; for components initialization. ProvideService, ProvideClient etc.&lt;/li&gt;
&lt;li&gt;Combine multiple inner provider dependencies into ProviderSet.&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;provider.go&lt;/code&gt; file in each component to hold all component initializations and ProviderSets in one place.&lt;/li&gt;
&lt;li&gt;When you use &lt;code&gt;wire.Bind(new(ServiceInterface), new(*Service))&lt;/code&gt; don’t forget about &lt;code&gt;*Service&lt;/code&gt;. It should be a pointer in most cases (if your methods are attached like &lt;code&gt;func (s *Service)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If you see the error “no provider found” - double check that component has all declared dependencies in &lt;code&gt;wire.Build&lt;/code&gt;. Also double-check the bindings of interfaces with structs and pointers.&lt;/li&gt;
&lt;li&gt;There are some uncertainties regarding how to manage configuration. The dependency injection pattern often leads to a scenario where each component has its own sources of configuration (with each component reading only its own environment variables). While this approach helps maintain low coupling, I find it cumbersome to have configuration scattered across multiple places and prefer to consolidate it into a single file. Therefore, my &lt;code&gt;ProvideConfig&lt;/code&gt; function will accept the core &lt;code&gt;Config&lt;/code&gt; as a parameter and retrieve only the necessary configuration values from it. However, I'm still undecided on the best approach here, so I'm open to suggestions.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>di</category>
      <category>google</category>
      <category>wire</category>
    </item>
    <item>
      <title>Long-term pitfalls of using Protobuf for Apache Kafka</title>
      <dc:creator>Sam Gozman</dc:creator>
      <pubDate>Sun, 31 Mar 2024 08:06:52 +0000</pubDate>
      <link>https://dev.to/samgozman/long-term-pitfalls-of-using-protobuf-for-apache-kafka-3j54</link>
      <guid>https://dev.to/samgozman/long-term-pitfalls-of-using-protobuf-for-apache-kafka-3j54</guid>
      <description>&lt;p&gt;In my new blog post, I would like to discuss the nuances I encountered while working on a legacy project that uses Protobuf for messages in Apache Kafka. Specifically, the issues that arose years later from this decision, how the diversity of programming languages in the project complicated things, and how the untimely updating of gRPC tools further magnified the problem. The events and issues described here occurred between 2022 and 2023.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input data.
&lt;/h2&gt;

&lt;p&gt;We had two monoliths in NodeJS &amp;amp; Ruby, a few dozen containerized apps in Golang and Python, five thousand outdated Protobuf schemas, a salt shaker half full of gRPC protocols, and a whole galaxy of multi-colored serializers, deserializers, structured and unstructured data… and also a quart of Kafka queues, a quart of proto-Kafka messages, a case of critical bugs, a pint of raw VSOP &lt;code&gt;protoc&lt;/code&gt; and two dozen highly outdated dependencies glued with DevOps love to over-complication.&lt;/p&gt;

&lt;p&gt;Not that we needed all that for the team of 40 devs, but once you get locked into a serious legacy code collection, the tendency is to push it as far as you can.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was the task?
&lt;/h2&gt;

&lt;p&gt;We needed to update the &lt;code&gt;protoc&lt;/code&gt; versions for five reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For NodeJS, &lt;code&gt;protoc&lt;/code&gt; and other gRPC tools were marked as deprecated and hadn't been updated in several years. New approaches have moved away from creating auto-generated files to working with Protobuf on the fly.&lt;/li&gt;
&lt;li&gt;In NodeJS services, developers wanted to type their work in Protobuf using TypeScript, and the old solution was incapable of generating types.&lt;/li&gt;
&lt;li&gt;We needed to access the &lt;code&gt;option&lt;/code&gt; syntax in Protobuf (&lt;code&gt;option &amp;lt;option_name&amp;gt; = &amp;lt;value&amp;gt;;&lt;/code&gt;) for Golang services (yes, our &lt;code&gt;protoc&lt;/code&gt; was that old).&lt;/li&gt;
&lt;li&gt;Our version of &lt;code&gt;protoc&lt;/code&gt; didn't work on ARM processors, which made development challenging for employees using Macs.&lt;/li&gt;
&lt;li&gt;It was one of the bottlenecks the company wanted to resolve in the process of updating old dependencies to reduce the number of CVEs in the images.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The root of the evil lay in the fact that the builder for Go binaries and the builder for NodeJS were moved out of the repositories into an external dependency controlled by DevOps. A dependency without proper versioning that was connected as a git submodule.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, what does Kafka have to do with it?
&lt;/h2&gt;

&lt;p&gt;The fact is that Protobuf was not only used for describing gRPC messages but also for Kafka messages. Often, the same contract applied in gRPC could be reused in Apache Kafka.&lt;/p&gt;

&lt;p&gt;But there are a few key differences between how gRPC and Kafka handle messages:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gRPC&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Typical gRPC messages are always binary, serialized using Protocol Buffers by default.&lt;/li&gt;
&lt;li&gt;gRPC tools (like &lt;code&gt;protoc&lt;/code&gt;) generate code to serialize and deserialize messages automatically based on the defined Protobuf message types.&lt;/li&gt;
&lt;li&gt;While binary is the default and highly recommended format for gRPC messages due to its efficiency, it's possible to use other serialization formats like JSON if needed. However, JSON serialization would require additional manual handling or third-party libraries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Kafka&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kafka messages are generally treated as binary payloads.&lt;/li&gt;
&lt;li&gt;Kafka itself does not enforce any specific data format on the messages.&lt;/li&gt;
&lt;li&gt;While Kafka itself is agnostic to the format of the messages it processes and stores, choosing an appropriate serialization format is crucial. Although Kafka messages are commonly serialized to binary for efficiency (to optimize for both size and speed) - alternate formats like JSON, Avro, or even plain text are entirely viable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flexibility in working with Kafka messages leads us to the fact that it does not have its own serializers in the way we are accustomed to with gRPC. The process of serialization and even the decision about its necessity lies entirely with the developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach.
&lt;/h2&gt;

&lt;p&gt;Of course, the most naive approach would have been to simply update the &lt;code&gt;protoc&lt;/code&gt; version and see what breaks. This was the quickest and most obvious option, and so we did just that.&lt;/p&gt;

&lt;p&gt;What broke? Pretty much everything 🙂&lt;/p&gt;

&lt;p&gt;Since I worked exclusively with NodeJS and Golang, I will speak about the problems that surfaced in my stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Golang
&lt;/h3&gt;

&lt;p&gt;The syntax for generating Protobuf files changed significantly.&lt;br&gt;
Old syntax example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  protoc &lt;span class="nt"&gt;--proto_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--go_out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;plugins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"grpc:&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New syntax example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  protoc &lt;span class="nt"&gt;--go_out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--go_opt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;source_relative &lt;span class="nt"&gt;--go-grpc_out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--go-grpc_opt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;source_relative &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the &lt;a href="https://github.com/grpc/grpc-go" rel="noopener noreferrer"&gt;grpc-go&lt;/a&gt; plugin is used for generating Protobuf. To generate Protobuf with a new plugin, it's now necessary to specify options for go, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;  &lt;span class="k"&gt;option&lt;/span&gt; &lt;span class="na"&gt;go_package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"github.com/your_company/your_package"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some methods from the generated proto files have changed, so code modifications were inevitable.&lt;/p&gt;

&lt;p&gt;Without specifying &lt;code&gt;go_package&lt;/code&gt;, Protobuf generates with an error. Adding &lt;code&gt;option&lt;/code&gt; broke compatibility with the old static generator in NodeJS (it didn't understand the syntax), significantly complicating the task. There was an alternative way to specify the go package through the &lt;code&gt;protoc&lt;/code&gt; console option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;  &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="na"&gt;go_opt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Mprotos&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="na"&gt;buzz.proto&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;github.com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;your_company&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;your_package&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But no one could get it to work; the command constantly failed with a syntax error. Likely, we made a mistake somewhere, but after unsuccessful attempts, we moved on.&lt;/p&gt;

&lt;h3&gt;
  
  
  NodeJS
&lt;/h3&gt;

&lt;p&gt;It was necessary to replace the npm library grpc with @grpc/grpc-js. The &lt;a href="https://github.com/grpc/grpc-node/tree/master/packages/grpc-tools" rel="noopener noreferrer"&gt;grpc-tools&lt;/a&gt; for NodeJS had not been maintained. It contains an outdated version of protoc.js and &lt;a href="https://github.com/protocolbuffers/protobuf/releases/tag/v3.19.1" rel="noopener noreferrer"&gt;protobuf v3.19.1&lt;/a&gt;. All replacements for this utility used the same outdated version of protoc &amp;amp; protobuf under the hood, which at the time did not have an ARM build and had several other differences from the latest versions.&lt;/p&gt;

&lt;p&gt;We started looking for a current replacement for grpc-tools for NodeJS and concluded that modern approaches in JS no longer offer file generation based on proto. Instead, they allow working on the fly, for example, &lt;a href="https://github.com/protobufjs/protobuf.js" rel="noopener noreferrer"&gt;protobuf.js&lt;/a&gt; and @grpc/proto-loader.&lt;/p&gt;

&lt;p&gt;Static generation, using &lt;code&gt;grpc-tools&lt;/code&gt; and &lt;code&gt;protoc&lt;/code&gt; for generation and &lt;code&gt;grpc&lt;/code&gt; or &lt;code&gt;@grpc/grpc-js&lt;/code&gt; in runtime, is basically deprecated by underlying outdated binaries and lack of GitHub activity. Dynamic generation, on the other hand, using &lt;code&gt;@grpc/proto-loader&lt;/code&gt; and &lt;code&gt;protobuf.js&lt;/code&gt; has become an industry standard.&lt;/p&gt;

&lt;p&gt;After several unsuccessful attempts to painlessly update grpc-tools for NodeJS to stick with the Static approach, we began exploring the Dynamic approach. Initially, everything went smoothly, working with &lt;code&gt;protobuf.js&lt;/code&gt; was easy and straightforward. But after a few days of rewriting code to work with gRPC, we suddenly remembered… &lt;strong&gt;that our Apache Kafka also uses Protobuf&lt;/strong&gt; alongside gRPC.&lt;/p&gt;

&lt;p&gt;Specifically, the &lt;code&gt;deserializeBinary&lt;/code&gt; and &lt;code&gt;serializeBinary&lt;/code&gt; methods from the generated files. The amount of code that needed to be rewritten to move from a static to a dynamic approach exceeded ten thousand lines of code, which was beyond our capabilities given the allocated time and the general reluctance to move in that direction. In all the languages that we supported on the project and in all the languages the team had previously worked with, the static approach of generating code based on proto contracts was always applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we have learned.
&lt;/h2&gt;

&lt;p&gt;Updating the approach to working with gRPC on NodeJS will take time, but it doesn't pose any complexity. I've rewritten one contract, and the rest will go smoothly. However, the task with Kafka-protobuf seems to be more challenging.&lt;/p&gt;

&lt;p&gt;Serializers and deserializers will have to be invented on our own. Those that can be borrowed from the generated code are not very readable and not at all versatile. As a lazy approach, of course, one could simply copy them and leave them somewhere inside the project, but then any changes to the existing structures would become complicated. We need to create a universal solution. The team did not consider moving away from protobuf in Kafka.&lt;/p&gt;

&lt;p&gt;With Golang, things also did not go as smoothly. Due to our failures with passing the package name in &lt;code&gt;go_opt&lt;/code&gt;, it will be necessary to add &lt;code&gt;option go_package&lt;/code&gt; in all existing contracts, with subsequent updates for consumers (contracts were connected as git submodules). An extremely tedious and monotonous task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion.
&lt;/h2&gt;

&lt;p&gt;Untimely updating of dependencies and neglected technical debt lead to long-lasting complex consequences. There are no perfect projects or products without technical debt, but that doesn't mean it shouldn't be addressed.&lt;/p&gt;

&lt;p&gt;For a development team that grew from ~20 people to ~40, the decision to develop everything on top of gRPC seems excessive. Despite all my love for gRPC, I believe that maintaining and developing service interactions through gRPC for a small team constantly prolonged the completion time for tasks. Using a simple HTTP router and describing the interaction schema through &lt;strong&gt;OpenAPI&lt;/strong&gt; (e.g., Swagger) would have significantly simplified the development and support of the API for a small team, while also generating excellent documentation for internal consumers.&lt;/p&gt;

&lt;p&gt;The latency between services was never a key problem in the speed of our product. The problems were more obvious and expected - a huge amount of N+1 issues, inefficient database operations etc.&lt;/p&gt;

&lt;p&gt;As for the use of Protobuf within Apache Kafka, I don't think it was a mistake. Serializing messages into compact binary data had advantages for us, as we had more than once hit Kafka topic size limits. Protobuf proved itself well as a unified format for describing structures between different programming languages.&lt;/p&gt;

&lt;p&gt;However, it's worth noting that the team avoided using types in Protobuf messages. All, or nearly all, fields were of type &lt;code&gt;string&lt;/code&gt;. So, in the end, I believe we could have just used JSON and sent it as binary, without the hassle (although it would have been less compact than Protobuf). JSON schemas can also be described and stored separately. JSON can be validated against a schema in all programming languages we use with predictable results.&lt;/p&gt;

&lt;p&gt;A key lesson from this experience, beyond the well-understood issues surrounding technical debt, is the importance of preparation when choosing to use Protobuf for Kafka messages. It's vital to anticipate the need for custom serialization and deserialization mechanisms well ahead of implementation. Even though Protobuf is not strictly tied to gRPC, most developers only apply it there, which undoubtedly leaves its mark on public libraries. Any use of Protobuf outside the context of gRPC should be &lt;strong&gt;considered unconventional&lt;/strong&gt;, meaning you need to avoid the bottleneck of relying solely on conventional gRPC-associated tools.&lt;/p&gt;

</description>
      <category>kafka</category>
      <category>protobuf</category>
      <category>go</category>
      <category>node</category>
    </item>
  </channel>
</rss>
