<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Go on</title><link>/post/go/</link><description>Recent content in Go on</description><generator>Hugo</generator><language>en</language><lastBuildDate>Mon, 09 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="/post/go/index.xml" rel="self" type="application/rss+xml"/><item><title>Lesson 10: Zero Values Are Useful — Go types that work before you touch them</title><link>/post/go/go-idioms-zero-values/</link><pubDate>Mon, 09 Mar 2026 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-zero-values/</guid><description>&lt;p&gt;In most languages, a freshly declared variable is either uninitialized garbage you can&amp;rsquo;t touch safely, or it needs a constructor call before it does anything useful. Go takes a different approach: every variable always has a value. When you don&amp;rsquo;t provide one, Go assigns the zero value for the type. What makes this interesting is that Go&amp;rsquo;s standard library is full of types designed so that their zero value is immediately useful — no constructor required.&lt;/p&gt;</description></item><item><title>Lesson 12: Value vs Pointer Receivers — The method that silently does nothing</title><link>/post/go/go-idioms-value-vs-pointer-receivers/</link><pubDate>Mon, 23 Feb 2026 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-value-vs-pointer-receivers/</guid><description>&lt;p&gt;Value vs pointer receivers is one of those topics that seems like a style preference until it silently breaks your program. The tell is a method that looks like it mutates a struct, compiles without complaint, but the mutations simply don&amp;rsquo;t persist. You add a log line, the value is right inside the method — and wrong the moment the method returns.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;A value receiver operates on a copy. Every time you call the method, Go copies the entire struct and passes the copy to the method. Mutations happen on the copy, which gets discarded when the method returns. The original is untouched.&lt;/p&gt;</description></item><item><title>Lesson 28: Production Concurrency Architecture — Putting it all together</title><link>/post/go/go-concurrency-production-architecture/</link><pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-production-architecture/</guid><description>&lt;p&gt;This is the lesson I wish existed when I started. Not &amp;ldquo;here&amp;rsquo;s how channels work&amp;rdquo; or &amp;ldquo;here&amp;rsquo;s a simple worker pool&amp;rdquo; — but the full picture: how you take everything you&amp;rsquo;ve learned about goroutines, channels, contexts, rate limiting, idempotency, observability, and graceful shutdown and assemble it into a service that actually belongs in production. One that handles failures, recovers gracefully, doesn&amp;rsquo;t leak resources, and gives you the visibility to understand what it&amp;rsquo;s doing.&lt;/p&gt;</description></item><item><title>Lesson 9: Testing with Real Databases — Mocking sql.DB is lying to yourself</title><link>/post/go/go-db-testing/</link><pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate><guid>/post/go/go-db-testing/</guid><description>&lt;p&gt;For years, the standard advice for testing Go database code was to use &lt;code&gt;sqlmock&lt;/code&gt; — a library that intercepts database calls and lets you assert which queries were run. I used it for a while. Then I started finding production bugs that my &lt;code&gt;sqlmock&lt;/code&gt; tests were actively hiding: constraint violations that only happen with real Postgres, query plan differences, JSON operator behavior, NULL handling edge cases, transaction isolation behavior. &lt;code&gt;sqlmock&lt;/code&gt; tests were passing while the same code was failing in production. That&amp;rsquo;s worse than no tests at all.&lt;/p&gt;</description></item><item><title>Lesson 19: Table-Driven Tests — One test function, fifty test cases</title><link>/post/go/go-idioms-table-driven-tests/</link><pubDate>Mon, 09 Feb 2026 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-table-driven-tests/</guid><description>&lt;p&gt;Most engineers know to write tests. Fewer think about how the test code itself should scale. When you need to cover thirty input variations of a function, duplicating the test body thirty times produces something that&amp;rsquo;s painful to read, painful to extend, and painful to debug when it fails. Table-driven tests are the pattern that scales. A slice of cases, one loop — your test code stays as clean as your production code.&lt;/p&gt;</description></item><item><title>Lesson 27: Idempotency in Concurrent Systems — Assume everything runs twice</title><link>/post/go/go-concurrency-idempotency/</link><pubDate>Sun, 08 Feb 2026 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-idempotency/</guid><description>&lt;p&gt;Every distributed system I&amp;rsquo;ve worked on eventually ran something twice. A retry after a timeout. A duplicate webhook delivery. A job that got processed by two workers simultaneously because of a clock skew issue in the claim timeout logic. A user who double-clicked a submit button. Systems aren&amp;rsquo;t gentle about this — they don&amp;rsquo;t ask &amp;ldquo;are you sure?&amp;rdquo; before running your code again. They just run it. The question isn&amp;rsquo;t whether your code will ever run twice. It&amp;rsquo;s whether running it twice causes a problem.&lt;/p&gt;</description></item><item><title>Lesson 8: Migrations Without Downtime — ALTER TABLE can lock your entire database</title><link>/post/go/go-db-migrations/</link><pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate><guid>/post/go/go-db-migrations/</guid><description>&lt;p&gt;The first time I caused a production outage with a database migration, I was adding a &lt;code&gt;NOT NULL&lt;/code&gt; column to a table. The migration looked innocent. It ran fine on staging with 1,000 rows. In production with 40 million rows, it locked the entire table for 11 minutes while Postgres rewrote every row. Every write to that table failed with a lock timeout. We rolled back the app but couldn&amp;rsquo;t roll back the migration. It was a terrible morning.&lt;/p&gt;</description></item><item><title>Lesson 22: Small Packages Win — One package, one job</title><link>/post/go/go-idioms-small-packages/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-small-packages/</guid><description>&lt;p&gt;There is a particular kind of Go codebase that&amp;rsquo;s immediately recognizable as written by someone still thinking in another language. It has a &lt;code&gt;utils&lt;/code&gt; package. Maybe a &lt;code&gt;common&lt;/code&gt; package. Possibly a &lt;code&gt;helpers&lt;/code&gt; folder with a file called &lt;code&gt;misc.go&lt;/code&gt;. Every function that doesn&amp;rsquo;t obviously belong somewhere ends up there, and over time these packages become the junk drawers of the codebase — bloated, unfocused, and imported by everything.&lt;/p&gt;
&lt;p&gt;Go has a better way. Small packages with narrow responsibilities. One package, one idea.&lt;/p&gt;</description></item><item><title>Lesson 26: Safe Background Jobs in Web Servers — Don''t spawn goroutines in handlers</title><link>/post/go/go-concurrency-safe-background-jobs/</link><pubDate>Fri, 23 Jan 2026 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-safe-background-jobs/</guid><description>&lt;p&gt;The first time I saw &lt;code&gt;go func()&lt;/code&gt; inside an HTTP handler, I thought it was clever. Fire off the slow work in the background, respond to the client fast, everybody wins. Then I watched what happened when we deployed a new version: the server started shutting down, half the background goroutines got killed mid-operation, we had partial writes in the database and no way to know which ones had completed. The &amp;ldquo;clever&amp;rdquo; optimization had created a correctness nightmare. The real problem wasn&amp;rsquo;t the goroutine — it was that it was invisible to the server&amp;rsquo;s shutdown lifecycle.&lt;/p&gt;</description></item><item><title>Lesson 25: Observability for Concurrency — You can''t fix what you can''t see</title><link>/post/go/go-concurrency-observability/</link><pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-observability/</guid><description>&lt;p&gt;The goroutine leak doesn&amp;rsquo;t announce itself. It just slowly inflates your memory graph — 50MB, 80MB, 150MB — until either your alerting fires or the OOM killer shows up. Then you&amp;rsquo;re staring at a core dump or (if you&amp;rsquo;re lucky) a live process trying to figure out which of the 10,000 goroutines running in your service is the culprit. I&amp;rsquo;ve been there. It&amp;rsquo;s not fun. The difference between spending 20 minutes diagnosing a leak and spending 4 hours diagnosing a leak is almost entirely whether you invested in observability before the incident.&lt;/p&gt;</description></item><item><title>Lesson 7: Slices Are Views, Not Arrays — Mutations you didn''t ask for</title><link>/post/go/go-idioms-slices-are-views/</link><pubDate>Mon, 12 Jan 2026 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-slices-are-views/</guid><description>&lt;p&gt;If you&amp;rsquo;re coming from Python, JavaScript, or Java, slices look familiar enough that you&amp;rsquo;ll assume you understand them. That assumption will hold right up until something mutates data you didn&amp;rsquo;t expect to be mutable, or a change you made inside a function mysteriously doesn&amp;rsquo;t show up outside it. Both surprises have the same root cause: a slice is not a copy of its data, it&amp;rsquo;s a window into an underlying array that may be shared with other slices.&lt;/p&gt;</description></item><item><title>Lesson 24: Testing Concurrent Code — Flaky tests mean flaky design</title><link>/post/go/go-concurrency-testing/</link><pubDate>Tue, 30 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-testing/</guid><description>&lt;p&gt;A flaky test is a lie your codebase tells you. It says &amp;ldquo;this sometimes works and sometimes doesn&amp;rsquo;t&amp;rdquo; and if you&amp;rsquo;re honest with yourself, you already know what that means: there&amp;rsquo;s a race condition somewhere, and the test is just unlucky enough to expose it occasionally. I used to mark flaky tests with &lt;code&gt;t.Skip(&amp;quot;flaky, fix later&amp;quot;)&lt;/code&gt; and move on. I stopped doing that when a race condition that a flaky test was hinting at caused a double-charge bug in production. Now I treat every flaky test as a production incident waiting to happen.&lt;/p&gt;</description></item><item><title>Lesson 25: Simplicity Is a Language Feature — Why Go says no so you can ship</title><link>/post/go/go-idioms-simplicity/</link><pubDate>Mon, 29 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-simplicity/</guid><description>&lt;p&gt;Every Go design decision that looks like a missing feature is actually a deliberate choice to remove cognitive overhead. No method overloading. No implicit conversions. One canonical formatter. Generics that arrived late and deliberately. Go&amp;rsquo;s simplicity is not an accident — it&amp;rsquo;s a feature, and one that pays compounding dividends as a codebase and team grow.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Languages that offer maximum expressiveness also offer maximum inconsistency. In Java or C++, method overloading sounds convenient:&lt;/p&gt;</description></item><item><title>Lesson 7: The N+1 Problem — One query per row is a performance bug</title><link>/post/go/go-db-n-plus-one/</link><pubDate>Thu, 25 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-n-plus-one/</guid><description>&lt;p&gt;The N+1 problem is the most common database performance issue I find when reviewing Go code, and it&amp;rsquo;s especially sneaky because it looks totally fine in development. You have a list of 10 users, you fetch their orders, 11 queries, no problem. You deploy to production, the table has 50,000 users, your endpoint suddenly takes 40 seconds, and you get a 3am page. The queries were always there — you just didn&amp;rsquo;t notice them until the data grew.&lt;/p&gt;</description></item><item><title>Lesson 23: Distributed vs Local Concurrency — Channels don''t cross process boundaries</title><link>/post/go/go-concurrency-distributed/</link><pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-distributed/</guid><description>&lt;p&gt;There&amp;rsquo;s a particular kind of confidence that comes after you&amp;rsquo;ve spent a few months writing Go. You&amp;rsquo;ve learned channels, you understand the happens-before guarantees, you&amp;rsquo;ve built a worker pool that hums along beautifully. Then someone says &amp;ldquo;we need to scale this across multiple pods&amp;rdquo; and you think: easy, I&amp;rsquo;ll just make the channels bigger. That thought has caused more production incidents than I care to remember — including one where we lost about 3,000 job records because a pod restarted mid-processing and nobody had thought about what &amp;ldquo;at-least-once delivery&amp;rdquo; actually means.&lt;/p&gt;</description></item><item><title>Lesson 17: select Is Elegant — Waiting on multiple futures without blocking</title><link>/post/go/go-idioms-select/</link><pubDate>Mon, 15 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-select/</guid><description>&lt;p&gt;Every non-trivial concurrent program eventually needs to wait on more than one thing at a time. Maybe you&amp;rsquo;re waiting on a job channel but also need to respond to cancellation. Maybe you want to fetch from a channel but bail out after a timeout. Sequential channel receives can&amp;rsquo;t do this — they block on one thing and miss everything else. &lt;code&gt;select&lt;/code&gt; is the solution, and it&amp;rsquo;s more powerful than it looks.&lt;/p&gt;</description></item><item><title>Lesson 22: Rate Limiting and Load Shedding — Say no before you fall over</title><link>/post/go/go-concurrency-rate-limiting/</link><pubDate>Wed, 10 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-rate-limiting/</guid><description>&lt;p&gt;The most honest thing a server can do is say &amp;ldquo;no.&amp;rdquo; Not crash, not time out after 30 seconds, not queue work indefinitely until memory explodes — just return a clean 503 and let the caller decide what to do. I spent a long time thinking rate limiting was about protecting &lt;em&gt;users&lt;/em&gt; from themselves. Then I got paged at midnight because a misbehaving client hammered an endpoint, the service queued everything politely, RAM climbed to the ceiling, and the process died. The fix wasn&amp;rsquo;t more memory. It was teaching the service to refuse work it couldn&amp;rsquo;t handle.&lt;/p&gt;</description></item><item><title>Lesson 6: Context with Database Queries — Every query needs a timeout</title><link>/post/go/go-db-context-queries/</link><pubDate>Wed, 03 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-context-queries/</guid><description>&lt;p&gt;We once had a query that ran for 47 minutes. I&amp;rsquo;m not joking. A reporting query that worked fine on the test dataset decided to do a full sequential scan on a 200M-row table in production because someone dropped an index by accident the night before. The query just ran. And ran. And ran. The connection was held the whole time, blocking the pool. New requests started queuing. Within 10 minutes, the service was effectively down — not because of an error, but because every database connection was held by queries waiting for that one slow one to finish.&lt;/p&gt;</description></item><item><title>Lesson 9: range Gotchas — The loop variable that bit every Go team</title><link>/post/go/go-idioms-range-gotchas/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-range-gotchas/</guid><description>&lt;p&gt;&lt;code&gt;range&lt;/code&gt; looks harmless. Index and value, loop over a slice — what could go wrong? Quite a bit, it turns out. Some of the most insidious bugs I&amp;rsquo;ve seen in Go codebases come from assumptions about &lt;code&gt;range&lt;/code&gt; that seem obvious but are wrong. One was painful enough that Go 1.22 changed the fundamental loop semantics to fix it. Let&amp;rsquo;s walk through each gotcha and the correct pattern to use.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;The classic range bug — the one that famously broke production code at teams large and small for years — is the loop variable capture problem. In Go versions before 1.22, every iteration of a &lt;code&gt;range&lt;/code&gt; loop reused the same loop variable. Taking its address multiple times gave you the same address every time.&lt;/p&gt;</description></item><item><title>Lesson 21: Supervisor Patterns — Let it crash, then restart it</title><link>/post/go/go-concurrency-supervisor-patterns/</link><pubDate>Tue, 25 Nov 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-supervisor-patterns/</guid><description>&lt;p&gt;Erlang got famous for the &amp;ldquo;let it crash&amp;rdquo; philosophy. The idea is that trying to handle every possible error in every possible place produces fragile, complicated code. It&amp;rsquo;s often better to let a process crash cleanly and have a supervisor restart it. The supervisor knows how to bring the process back to a known-good state. The process itself just needs to do its job and fail fast when something&amp;rsquo;s wrong.&lt;/p&gt;</description></item><item><title>Lesson 24: Prefer Plain Structs — Boring code is correct code</title><link>/post/go/go-idioms-plain-structs/</link><pubDate>Mon, 17 Nov 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-plain-structs/</guid><description>&lt;p&gt;There is a particular brand of cleverness that feels deeply satisfying to write and deeply painful to maintain. The &lt;code&gt;AbstractServiceProviderFactory&lt;/code&gt;. The builder that returns a builder that configures a builder. The generic interface so abstract it could model anything and therefore models nothing well. Go&amp;rsquo;s culture pushes back hard against this tendency, and for good reason: I&amp;rsquo;ve seen more bugs traced to abstraction layers than to simple structs.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;The most common form of over-engineering in Go is attempting to import Java-style construction patterns. Go doesn&amp;rsquo;t have constructors, so engineers invent them — and the result is code that&amp;rsquo;s harder to configure, not easier:&lt;/p&gt;</description></item><item><title>Lesson 20: Profiling Contention — pprof knows where your goroutines sleep</title><link>/post/go/go-concurrency-contention-profiling/</link><pubDate>Thu, 13 Nov 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-contention-profiling/</guid><description>&lt;p&gt;You&amp;rsquo;ve added goroutines, you&amp;rsquo;ve got worker pools, you&amp;rsquo;ve written careful concurrent code — and the service is still slower than expected. Maybe goroutine count is climbing in your metrics dashboard. Maybe p99 latency has a long tail you can&amp;rsquo;t explain. Maybe a throughput test plateaus at 40% of what you thought the hardware should support.&lt;/p&gt;
&lt;p&gt;This is where guessing stops and profiling starts. Go ships world-class concurrency profiling tools in the standard library — mutex profiles, block profiles, goroutine dumps, and the execution tracer. Most engineers know about the CPU and memory profiler. Far fewer use the concurrency-specific profiles, which is a shame because they find the exact thing that&amp;rsquo;s wrong in minutes instead of days.&lt;/p&gt;</description></item><item><title>Lesson 11: Nil Slice vs Empty Slice — Same length, different meaning</title><link>/post/go/go-idioms-nil-vs-empty-slice/</link><pubDate>Mon, 03 Nov 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-nil-vs-empty-slice/</guid><description>&lt;p&gt;Go has two ways to have a slice with zero elements, and they are not the same thing. Developers coming from Python, Ruby, or JavaScript expect an empty collection to just be an empty collection. In Go, the distinction between a nil slice and an empty slice is subtle enough that you can miss it for months — right up until a frontend engineer files a bug because your API is returning &lt;code&gt;null&lt;/code&gt; instead of &lt;code&gt;[]&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Lesson 19: Ordering vs Throughput — You can have fast or ordered, pick one</title><link>/post/go/go-concurrency-ordering-throughput/</link><pubDate>Fri, 31 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-ordering-throughput/</guid><description>&lt;p&gt;Here&amp;rsquo;s a tension that comes up in almost every real concurrent system: you want to process things fast, which means doing them in parallel, but the output needs to come out in the same order the input arrived. These two goals are in direct conflict. Parallel execution means things finish in unpredictable order. Ordered output means you have to wait for the slowest thing in each batch.&lt;/p&gt;
&lt;p&gt;The engineers who understand this tension build systems that make the trade-off explicitly. The engineers who don&amp;rsquo;t notice it build systems that either throttle themselves to single-goroutine throughput &amp;ldquo;to preserve order&amp;rdquo; or silently return results in the wrong order and wonder why tests fail non-deterministically.&lt;/p&gt;</description></item><item><title>Lesson 5: sqlc vs ORM vs Raw SQL — Pick your tradeoff, not your religion</title><link>/post/go/go-db-sqlc-vs-orm/</link><pubDate>Wed, 29 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-sqlc-vs-orm/</guid><description>&lt;p&gt;Every time someone asks &amp;ldquo;should I use GORM or raw SQL?&amp;rdquo; a flame war breaks out. I&amp;rsquo;ve been on both sides of that argument, and I&amp;rsquo;ve shipped production systems using all four approaches — raw SQL, GORM, sqlc, and Ent. My opinion now is boring: each one is the right choice in a specific context, and none of them is universally correct. The question isn&amp;rsquo;t which one is best, it&amp;rsquo;s which tradeoffs you&amp;rsquo;re signing up for.&lt;/p&gt;</description></item><item><title>Lesson 18: sync.Mutex Is Often Simpler — Not everything needs a channel</title><link>/post/go/go-idioms-mutex-simpler/</link><pubDate>Mon, 20 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-mutex-simpler/</guid><description>&lt;p&gt;After you absorb the Go concurrency philosophy — share memory by communicating — there&amp;rsquo;s a temptation to reach for channels every time two goroutines need to share data. Resist that. Channels are for coordination and ownership transfer. For shared mutable state that multiple goroutines read and write, a mutex is usually clearer, simpler, and faster. Using a channel where a mutex belongs is one of those things that looks idiomatic but isn&amp;rsquo;t.&lt;/p&gt;</description></item><item><title>Lesson 18: Backpressure Design — Slow down or blow up</title><link>/post/go/go-concurrency-backpressure/</link><pubDate>Sun, 19 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-backpressure/</guid><description>&lt;p&gt;Every system has a throughput ceiling. The question isn&amp;rsquo;t whether your service can be overwhelmed — it can. The question is what happens when it is. Does it slow down gracefully, maintaining correctness and giving the caller a clear signal? Or does it blow up — OOM-killed, goroutines piling up, latency spiking to infinity while requests pile up in an unbounded queue?&lt;/p&gt;
&lt;p&gt;Backpressure is the mechanism that answers that question. It&amp;rsquo;s how a component communicates to its upstream: &amp;ldquo;I&amp;rsquo;m full — slow down.&amp;rdquo; Without it, fast producers eat slow consumers alive. In Go, backpressure is naturally expressed through blocking channel sends — and understanding when to block versus when to drop is one of the more interesting design decisions in concurrent systems.&lt;/p&gt;</description></item><item><title>Lesson 17: Go Scheduler Behavior — M:N scheduling is not magic</title><link>/post/go/go-concurrency-scheduler/</link><pubDate>Wed, 08 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-scheduler/</guid><description>&lt;p&gt;Most Go developers write concurrent code for years without thinking about the scheduler. That&amp;rsquo;s by design — the scheduler is supposed to be invisible. But eventually you&amp;rsquo;ll hit a situation where goroutines aren&amp;rsquo;t running when you expect them to, CPU cores are idle while goroutines pile up, or a CPU-bound workload is somehow slower with more goroutines. At that point you need a mental model of what&amp;rsquo;s actually happening under the hood.&lt;/p&gt;</description></item><item><title>Lesson 3: Multiple Return Values — Go functions don't hide their failures</title><link>/post/go/go-idioms-multiple-return-values/</link><pubDate>Mon, 06 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-multiple-return-values/</guid><description>&lt;p&gt;In most languages, a function returns one thing and communicates failure through a side channel — an exception, a null, a magic sentinel value. Go&amp;rsquo;s approach is different: functions can return multiple values, and the convention is to use that to make failure explicit in the signature itself. Once you&amp;rsquo;ve used it for a while, hiding failures in side channels starts to feel dishonest.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Sentinel values are the old-school way to signal failure from a function. You pick some value that &amp;ldquo;shouldn&amp;rsquo;t&amp;rdquo; appear in normal results and treat it as an error signal:&lt;/p&gt;</description></item><item><title>Lesson 4: The Repository Pattern — Sometimes a function is enough</title><link>/post/go/go-db-repository-pattern/</link><pubDate>Thu, 02 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-repository-pattern/</guid><description>&lt;p&gt;The repository pattern is one of those ideas that sounds great in an architecture talk and causes real pain when applied indiscriminately to a Go codebase. I&amp;rsquo;ve seen teams add a &lt;code&gt;UserRepository&lt;/code&gt; interface with five methods, a concrete &lt;code&gt;postgresUserRepository&lt;/code&gt; implementation, and a &lt;code&gt;mockUserRepository&lt;/code&gt; for tests — and then wonder why everything takes three times as long to write. I&amp;rsquo;ve also seen teams skip the pattern entirely and end up with &lt;code&gt;*sql.DB&lt;/code&gt; threaded through 40 different functions, impossible to test without a real database.&lt;/p&gt;</description></item><item><title>Lesson 29: sync.Cond — The coordination primitive nobody teaches</title><link>/post/go/go-concurrency-sync-cond/</link><pubDate>Wed, 01 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-sync-cond/</guid><description>&lt;p&gt;Every Go concurrency course covers goroutines, channels, mutexes, WaitGroups, and maybe semaphores. Very few touch &lt;code&gt;sync.Cond&lt;/code&gt;. It&amp;rsquo;s either treated as advanced or dismissed as unnecessary because &amp;ldquo;just use channels.&amp;rdquo; But there&amp;rsquo;s a whole class of coordination problem where channels are the wrong tool and &lt;code&gt;sync.Cond&lt;/code&gt; is exactly right. Once you see the pattern, you&amp;rsquo;ll recognize it everywhere — and you&amp;rsquo;ll stop reaching for &lt;code&gt;time.Sleep&lt;/code&gt; polling loops when you shouldn&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;This is a bonus lesson. Not because the topic is minor, but because it builds on mutexes (Lesson 6) and you really need to understand lock ownership before &lt;code&gt;sync.Cond&lt;/code&gt; clicks.&lt;/p&gt;</description></item><item><title>Lesson 16: Atomic Operations — Lock-free when you can, mutex when you must</title><link>/post/go/go-concurrency-atomics/</link><pubDate>Mon, 29 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-atomics/</guid><description>&lt;p&gt;The first time I looked at &lt;code&gt;sync/atomic&lt;/code&gt;, it felt like a niche tool for systems programmers writing lock-free data structures. Turns out it&amp;rsquo;s one of the most practically useful packages in the standard library — and most Go developers reach for it way too late, after they&amp;rsquo;ve already built something with a mutex and then profiled it into submission.&lt;/p&gt;
&lt;p&gt;Atomic operations are CPU-level instructions that read or modify memory as a single, indivisible unit. There&amp;rsquo;s no scheduler window between the read and the write. That means multiple goroutines can operate on the same memory location without a lock — and without any blocking. When your shared state is a counter, a flag, or a single configuration value, atomics are almost always the right tool.&lt;/p&gt;</description></item><item><title>Lesson 13: iota for Enums — Constants that count themselves</title><link>/post/go/go-idioms-iota-enums/</link><pubDate>Mon, 22 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-iota-enums/</guid><description>&lt;p&gt;Go doesn&amp;rsquo;t have a built-in enum keyword. What it has is &lt;code&gt;iota&lt;/code&gt;, a constant counter that resets to zero at the start of each &lt;code&gt;const&lt;/code&gt; block and increments with every constant declaration. It sounds underwhelming. In practice it gives you typed enums, bitmask permissions, and self-maintaining constant sequences — all without any runtime overhead.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;The naive approach to enums in Go is plain integer constants or string constants. Both work, but neither gives you type safety:&lt;/p&gt;</description></item><item><title>Lesson 15: Semaphores for Concurrency Limits — A channel with a size is a semaphore</title><link>/post/go/go-concurrency-semaphores/</link><pubDate>Thu, 18 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-semaphores/</guid><description>&lt;p&gt;There&amp;rsquo;s a class of production bugs I see over and over — and the cause is almost always the same: nothing is telling the program to &lt;em&gt;slow down&lt;/em&gt;. A spike in traffic arrives, every goroutine blasts outbound, and suddenly you&amp;rsquo;ve got five hundred simultaneous connections against a Postgres instance that&amp;rsquo;s configured for a hundred. The database starts rejecting connections. The application throws errors. Everyone&amp;rsquo;s paged at 2am.&lt;/p&gt;
&lt;p&gt;The fix isn&amp;rsquo;t complicated. It&amp;rsquo;s a semaphore — a primitive that limits how many concurrent operations are in flight at once. Go doesn&amp;rsquo;t ship a dedicated semaphore type, but it doesn&amp;rsquo;t need to. A buffered channel of the right size &lt;em&gt;is&lt;/em&gt; a semaphore, and that insight unlocks a whole class of resource-limiting patterns.&lt;/p&gt;</description></item><item><title>Lesson 20: internal Package Is Underrated — Compiler-enforced privacy for free</title><link>/post/go/go-idioms-internal-package/</link><pubDate>Mon, 08 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-internal-package/</guid><description>&lt;p&gt;Go has two visibility levels: exported (starts with a capital letter) and unexported (doesn&amp;rsquo;t). Most engineers use only these two. But there&amp;rsquo;s a third option that the language gives you for free, and it&amp;rsquo;s more useful than most people realize. The &lt;code&gt;internal&lt;/code&gt; directory enforces that certain packages can only be imported by code within your own module — and the compiler, not documentation or convention, does the enforcing.&lt;/p&gt;
&lt;p&gt;Exported symbols are API commitments. Once something is exported and external code is depending on it, changing it is a breaking change. The &lt;code&gt;internal&lt;/code&gt; package is the escape hatch: share code across multiple packages in your own codebase without accidentally publishing an API surface you&amp;rsquo;ll have to maintain forever.&lt;/p&gt;</description></item><item><title>Lesson 3: Transactions That Don't Bite — Begin, defer rollback, commit</title><link>/post/go/go-db-transactions/</link><pubDate>Sun, 07 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-transactions/</guid><description>&lt;p&gt;Transactions are the part of database programming where &amp;ldquo;it&amp;rsquo;s fine most of the time&amp;rdquo; really isn&amp;rsquo;t good enough. A buggy SELECT just returns wrong data. A buggy transaction can leave your database in a half-written state — an order placed without inventory decremented, money debited without the transfer completing, a user created without their profile record. The bugs are subtle, often don&amp;rsquo;t manifest in testing, and only show up in production when two things happen at the same time.&lt;/p&gt;</description></item><item><title>Lesson 14: errgroup for Structured Concurrency — All succeed or all cancel</title><link>/post/go/go-concurrency-errgroup/</link><pubDate>Tue, 02 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-errgroup/</guid><description>&lt;p&gt;There&amp;rsquo;s a pattern that comes up constantly in backend services: make N concurrent calls, collect all their results, and if any one of them fails, cancel the rest and return the error. This is the &amp;ldquo;all succeed or all cancel&amp;rdquo; pattern — and before &lt;code&gt;errgroup&lt;/code&gt;, implementing it correctly required a non-trivial amount of boilerplate involving &lt;code&gt;WaitGroup&lt;/code&gt;, error channels, and manual context cancellation. People got it wrong often enough that &lt;code&gt;errgroup&lt;/code&gt; was created specifically to handle it.&lt;/p&gt;</description></item><item><title>Lesson 13: Timeouts Everywhere — Unbounded waits are production bugs</title><link>/post/go/go-concurrency-timeouts/</link><pubDate>Tue, 26 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-timeouts/</guid><description>&lt;p&gt;I have a strongly held opinion about timeouts: if you&amp;rsquo;re making a network call, a database query, or waiting on any external resource without a timeout, you&amp;rsquo;ve written a production bug. It just hasn&amp;rsquo;t fired yet. The network will eventually hang. The database will eventually have a slow query. The external API will eventually stop responding. And when it does, your goroutine will wait. And wait. And wait — holding a connection, a file descriptor, a slot in your worker pool — until the process runs out of resources or someone restarts it.&lt;/p&gt;</description></item><item><title>Lesson 5: Implicit Interfaces — The best decoupling you'll never declare</title><link>/post/go/go-idioms-implicit-interfaces/</link><pubDate>Mon, 25 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-implicit-interfaces/</guid><description>&lt;p&gt;In Java or C#, you declare that a class implements an interface. You write &lt;code&gt;implements Runnable&lt;/code&gt;, and the compiler ties that class to that interface forever. Go doesn&amp;rsquo;t work that way. A type satisfies an interface the moment it has the right methods — no declaration, no explicit relationship. This sounds like a minor syntactic difference, but it changes how you design systems in ways that compound over time.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;When interfaces are declared by the implementor (the Java way), you end up with a few recurring problems.&lt;/p&gt;</description></item><item><title>Lesson 12: Leak Prevention — Every goroutine must have an exit</title><link>/post/go/go-concurrency-leak-prevention/</link><pubDate>Fri, 15 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-leak-prevention/</guid><description>&lt;p&gt;Goroutine leaks are the memory leaks of concurrent Go. They&amp;rsquo;re slow, invisible, and tend to surface only under load — or worse, only after days of continuous running when the service has accumulated tens of thousands of stuck goroutines. I&amp;rsquo;ve debugged two production incidents that traced back to leaks in code that had been in production for months, completely unnoticed during normal load.&lt;/p&gt;
&lt;p&gt;The root cause is almost always the same: someone started a goroutine and didn&amp;rsquo;t give it a way to exit. Not a way to exit eventually — a way to exit in every possible code path.&lt;/p&gt;</description></item><item><title>Lesson 15: Goroutines Are Cheap, Not Free — 2KB that can eat your server</title><link>/post/go/go-idioms-goroutines-cheap-not-free/</link><pubDate>Mon, 11 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-goroutines-cheap-not-free/</guid><description>&lt;p&gt;&amp;ldquo;Goroutines are cheap&amp;rdquo; is something you read in every Go introduction. It&amp;rsquo;s true. A goroutine starts with a 2KB stack and the runtime handles scheduling. Spinning up a thousand of them is trivial. The part the introductions leave out is that &amp;ldquo;cheap&amp;rdquo; is not &amp;ldquo;free,&amp;rdquo; and goroutines that you start and never stop are a leak — one that doesn&amp;rsquo;t crash your program, just slowly eats your memory and degrades your scheduler until something gives.&lt;/p&gt;</description></item><item><title>Lesson 2: Connection Pool Tuning — Your pool config is probably wrong</title><link>/post/go/go-db-connection-pools/</link><pubDate>Fri, 08 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-connection-pools/</guid><description>&lt;p&gt;Here&amp;rsquo;s a scenario that played out for me once: service is running fine for weeks, then we double traffic, and suddenly requests start timing out. Not all of them — maybe 10%. The logs show &lt;code&gt;pq: sorry, too many clients already&lt;/code&gt;. Postgres is refusing connections. But we have a connection pool — that&amp;rsquo;s the whole point, right? Why is Postgres seeing more connections than it can handle?&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;database/sql&lt;/code&gt;&amp;rsquo;s default pool settings are almost certainly wrong for production, and most people never change them.&lt;/p&gt;</description></item><item><title>Lesson 11: Graceful Shutdown — Stop accepting, finish what you started</title><link>/post/go/go-concurrency-graceful-shutdown/</link><pubDate>Sun, 03 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-graceful-shutdown/</guid><description>&lt;p&gt;Most Go services handle startup carefully and shutdown carelessly. The startup code has retries, health checks, dependency validation. The shutdown code is &lt;code&gt;os.Exit(0)&lt;/code&gt; or — if the developer was feeling generous — nothing at all, just letting the process get killed. That&amp;rsquo;s how you get dropped HTTP connections, half-written database records, uncommitted Kafka offsets, and on-call alerts at 3am.&lt;/p&gt;
&lt;p&gt;Graceful shutdown has one principle: stop accepting new work, finish what you already started. That&amp;rsquo;s it. But implementing it correctly requires knowing the shutdown order, wiring up OS signals properly, and draining workers before pulling the plug.&lt;/p&gt;</description></item><item><title>Lesson 23: Error Values, Not Exceptions — Errors you can actually inspect</title><link>/post/go/go-idioms-error-values/</link><pubDate>Mon, 28 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-error-values/</guid><description>&lt;p&gt;In most languages, errors are events — they get thrown, they propagate up the call stack, and you catch them somewhere above. Go rejects this model entirely. In Go, an error is a value, just like an integer or a string. You pass it around, inspect it, wrap it with context, and check it right where it happens. Ignore it and your code doesn&amp;rsquo;t crash loudly; it quietly does the wrong thing, and you&amp;rsquo;ll find out at the worst possible moment.&lt;/p&gt;</description></item><item><title>Lesson 10: Pipelines and Stage Isolation — Each stage owns its output channel</title><link>/post/go/go-concurrency-pipelines/</link><pubDate>Thu, 17 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-pipelines/</guid><description>&lt;p&gt;A pipeline is how you turn a stream of work into a sequence of transformations without writing a single monolithic function that does everything. Each stage reads from one channel, does its thing, and writes to another. Stages compose. Stages are independently testable. And because each stage runs concurrently with every other stage, the whole pipeline processes multiple items simultaneously — like an assembly line rather than a factory worker doing each product start to finish.&lt;/p&gt;</description></item><item><title>Lesson 1: Error Handling — Your code is lying if it ignores errors</title><link>/post/go/go-idioms-error-handling/</link><pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-error-handling/</guid><description>&lt;p&gt;If you&amp;rsquo;re coming from Python, Java, or JavaScript, Go&amp;rsquo;s error handling will feel strange at first. There&amp;rsquo;s no &lt;code&gt;try/catch&lt;/code&gt;. No exceptions bubbling up the call stack. Instead, errors are just values — and you deal with them right where they happen. Ignore them and your code doesn&amp;rsquo;t crash loudly; it quietly lies to you, and you won&amp;rsquo;t find out until 2am when production is on fire.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;The blank identifier &lt;code&gt;_&lt;/code&gt; is the most dangerous character in Go. Here&amp;rsquo;s what it looks like when engineers first start writing Go:&lt;/p&gt;</description></item><item><title>Lesson 6: Agent Architectures — Building autonomous agents in Go</title><link>/post/go/go-ai-agent-architectures/</link><pubDate>Thu, 10 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-ai-agent-architectures/</guid><description>&lt;p&gt;An agent is a loop: observe, think, act, repeat. The LLM is the &amp;ldquo;think&amp;rdquo; step — it decides what to do next given the current state. Your Go code handles &amp;ldquo;observe&amp;rdquo; (gathering context), &amp;ldquo;act&amp;rdquo; (executing tool calls), and the loop control that keeps everything running. I&amp;rsquo;ve built agents that write and execute code, agents that browse the web, and agents that orchestrate multi-step data pipelines. The underlying architecture is always the same few patterns, and Go&amp;rsquo;s concurrency makes the execution layer clean and fast.&lt;/p&gt;</description></item><item><title>Lesson 9: Worker Pools — Bounded concurrency or bust</title><link>/post/go/go-concurrency-worker-pools/</link><pubDate>Thu, 10 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-worker-pools/</guid><description>&lt;p&gt;Production Go codebases that run fine in staging will sometimes crater under real traffic. More often than not, the cause is unbounded goroutine creation. Some background job processor spins up a goroutine per message. The queue backs up. Suddenly there are 50,000 goroutines fighting for CPU, exhausting file descriptors, allocating gigabytes of stack space. The service falls over. The incident post-mortem says &amp;ldquo;we didn&amp;rsquo;t expect this load.&amp;rdquo; The real cause: no worker pool.&lt;/p&gt;</description></item><item><title>Lesson 1: database/sql Done Right — The stdlib is better than you think</title><link>/post/go/go-db-sql-done-right/</link><pubDate>Tue, 08 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-sql-done-right/</guid><description>&lt;p&gt;I spent a long time reaching for third-party database libraries in Go before I actually read the &lt;code&gt;database/sql&lt;/code&gt; docs. When I finally did, I was embarrassed — the stdlib had everything I needed and I&amp;rsquo;d been adding dependencies for no reason. The problem isn&amp;rsquo;t that &lt;code&gt;database/sql&lt;/code&gt; is limited. The problem is that it has a handful of non-obvious behaviors that, if you don&amp;rsquo;t know about them, will burn you in production. Once you internalize those, you&amp;rsquo;ll write better database code than most people using ORMs.&lt;/p&gt;</description></item><item><title>Lesson 8: strings and bytes Builders — Stop concatenating in loops</title><link>/post/go/go-stdlib-strings-bytes/</link><pubDate>Sat, 05 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-stdlib-strings-bytes/</guid><description>&lt;p&gt;String concatenation with &lt;code&gt;+&lt;/code&gt; is one of the most common performance bugs I see in Go code reviews. Not because developers are careless, but because the bug is invisible — the code looks clean and correct. &lt;code&gt;result += piece&lt;/code&gt; is obvious. The quadratic memory behavior that follows is not.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;strings&lt;/code&gt; and &lt;code&gt;bytes&lt;/code&gt; packages are where Go provides the right tools for this job. &lt;code&gt;strings.Builder&lt;/code&gt; and &lt;code&gt;bytes.Buffer&lt;/code&gt; are both efficient for incremental string construction, but they&amp;rsquo;re not the same type and the choice between them matters in specific cases. Beyond building strings, these packages contain functions that have non-obvious but significant performance implications: &lt;code&gt;strings.Contains&lt;/code&gt; vs &lt;code&gt;strings.Index&lt;/code&gt;, &lt;code&gt;strings.Split&lt;/code&gt; vs &lt;code&gt;strings.SplitN&lt;/code&gt;, and the ones people reach for that they shouldn&amp;rsquo;t.&lt;/p&gt;</description></item><item><title>Lesson 10: Over-Engineering CRUD — Not every API needs a hexagonal architecture</title><link>/post/go/go-anti-over-engineering/</link><pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-over-engineering/</guid><description>&lt;p&gt;There is a category of service that I have built many times and will build many more times: an API that creates, reads, updates, and deletes records in a relational database, with some validation and authentication. There is nothing glamorous about it, and there is nothing architecturally complex about it either. The correct implementation is straightforward, fast to build, easy to test, and easy to read. The over-engineered implementation has event sourcing, CQRS, a message bus, six layers of abstraction, and takes three months to build something that the straightforward implementation would have shipped in two weeks.&lt;/p&gt;</description></item><item><title>Lesson 2: defer for Cleanup — Put the cleanup next to the mess</title><link>/post/go/go-idioms-defer-cleanup/</link><pubDate>Mon, 30 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-defer-cleanup/</guid><description>&lt;p&gt;Every time you open a file, acquire a lock, or start a database transaction, you&amp;rsquo;ve created a resource that needs to be released when you&amp;rsquo;re done with it. In most languages you manage this with &lt;code&gt;finally&lt;/code&gt; blocks or RAII patterns. Miss one cleanup and you&amp;rsquo;ve got a leak. Go has &lt;code&gt;defer&lt;/code&gt;, and once it clicks, you&amp;rsquo;ll wonder how you ever coded without it.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s what resource cleanup looks like without &lt;code&gt;defer&lt;/code&gt;. Real code, the kind that ships:&lt;/p&gt;</description></item><item><title>Lesson 8: Fan-Out / Fan-In — Distribute, collect, don''t leak</title><link>/post/go/go-concurrency-fan-out-fan-in/</link><pubDate>Tue, 24 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-fan-out-fan-in/</guid><description>&lt;p&gt;There&amp;rsquo;s a moment in every Go developer&amp;rsquo;s journey where the serial loop stops being acceptable. You&amp;rsquo;ve got 200 URLs to fetch, 500 records to transform, 50 API calls to make — and you&amp;rsquo;re doing them one at a time. The fix feels obvious: spin up goroutines. But spin them up without a plan and you&amp;rsquo;ve traded one problem for three.&lt;/p&gt;
&lt;p&gt;Fan-out / fan-in is the pattern that makes parallel work manageable. Fan-out means distributing a stream of work across multiple goroutines. Fan-in means collecting their results into a single channel. Simple in concept, surprisingly easy to get wrong in practice.&lt;/p&gt;</description></item><item><title>Lesson 10: The Complete Go Service — HTTP + DB + workers + shutdown in 200 lines</title><link>/post/go/go-api-complete-service/</link><pubDate>Fri, 20 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-api-complete-service/</guid><description>&lt;p&gt;Every lesson in this series has been a piece of a puzzle. HTTP routing, middleware, validation, error responses, pagination, idempotency, timeouts, rate limiting, config management — each one addresses a specific concern in isolation. This final lesson puts them together into a single, production-shaped service that you can actually use as a starting point.&lt;/p&gt;
&lt;p&gt;The goal is not a complete application. It is a skeleton that demonstrates how all the pieces wire together in &lt;code&gt;main.go&lt;/code&gt; and what a well-structured Go service looks like before you add your business logic.&lt;/p&gt;</description></item><item><title>Lesson 6: API Gateway Patterns — One entry point, many backends</title><link>/post/go/go-micro-api-gateway/</link><pubDate>Wed, 18 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-micro-api-gateway/</guid><description>&lt;p&gt;The first time I connected a mobile frontend directly to 11 microservices, I created 11 places for the mobile team to integrate, 11 different auth schemes to understand, 11 different error formats to handle, and a situation where a single product screen required 6 parallel API calls because the data was spread across 6 services. An API gateway solves all of this: one URL, one auth scheme, one error format, and the ability to aggregate multiple service responses into a single response the client actually needs.&lt;/p&gt;</description></item><item><title>Lesson 14: context.Context — The parameter every function should take first</title><link>/post/go/go-idioms-context/</link><pubDate>Mon, 16 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-context/</guid><description>&lt;p&gt;Without &lt;code&gt;context.Context&lt;/code&gt;, a function that makes a database call, fires an HTTP request, or runs a long computation has no way to be told to stop. The caller times out, the user closes the browser tab, the load balancer kills the connection — and your function keeps running, consuming CPU and holding database connections, doing work that nobody will ever see. &lt;code&gt;context.Context&lt;/code&gt; is how Go solves this.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;A function with no context cannot be cancelled. It runs until it finishes, or until the process dies. In an HTTP server handling hundreds of requests per second, this accumulates fast:&lt;/p&gt;</description></item><item><title>Lesson 8: Zero-Downtime Deploys — Rolling updates without dropping requests</title><link>/post/go/go-deploy-zero-downtime/</link><pubDate>Sun, 15 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-deploy-zero-downtime/</guid><description>&lt;p&gt;The first rolling deployment I did without graceful shutdown handling produced about 200 errors for in-flight requests. Kubernetes sent &lt;code&gt;SIGTERM&lt;/code&gt; to the old pods, the Go process exited immediately, and every active request was terminated mid-flight. The users got 502s. The monitoring dashboard lit up red for about 30 seconds per deploy, every time.&lt;/p&gt;
&lt;p&gt;Kubernetes&amp;rsquo;s rolling update strategy replaces pods one at a time and can achieve zero request drops — but only if your application cooperates. The contract is: Kubernetes sends &lt;code&gt;SIGTERM&lt;/code&gt;, your application finishes in-flight requests, then exits. If you ignore &lt;code&gt;SIGTERM&lt;/code&gt; and exit immediately, Kubernetes kills you with &lt;code&gt;SIGKILL&lt;/code&gt; after the grace period anyway, and you drop the requests. The work is in your application code, not in the Kubernetes config.&lt;/p&gt;</description></item><item><title>Lesson 8: Production Error Architecture — Designing the error system for a real service</title><link>/post/go/go-errors-production-architecture/</link><pubDate>Sat, 14 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-errors-production-architecture/</guid><description>&lt;p&gt;This is the lesson where everything comes together. Over the previous seven lessons we&amp;rsquo;ve looked at sentinels and typed errors, wrapping strategy, error classification, where to log, how to translate at layer boundaries, and when panic is actually defensible. Now I want to show you what all of that looks like assembled into a complete, production-ready error system for a real API service.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m going to build the full error architecture for a hypothetical orders API. By the end you&amp;rsquo;ll have a template you can adapt directly — the error type hierarchy, the middleware, the structured logging, and the client-facing error codes. This is the code I wish I&amp;rsquo;d had when I started building services in Go.&lt;/p&gt;</description></item><item><title>Lesson 7: Race Conditions and the Go Memory Model — The bug you can't reproduce</title><link>/post/go/go-concurrency-race-conditions/</link><pubDate>Thu, 12 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-race-conditions/</guid><description>&lt;p&gt;Race conditions are the category of bug that makes senior engineers paranoid and junior engineers dismissive. They don&amp;rsquo;t reproduce consistently. Your tests pass. Your staging environment looks fine. Then, in production, under a specific load pattern at a specific time, two goroutines read and write the same memory address with no synchronization, and you get corrupted data — or a panic — or silently wrong results that sit in your database for three weeks before someone notices. Understanding why they happen at the language level is what separates engineers who prevent them from engineers who just get lucky.&lt;/p&gt;</description></item><item><title>Lesson 7: go:generate and Code Generation — Let the machine write the boring code</title><link>/post/go/go-reflect-codegen/</link><pubDate>Tue, 10 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-reflect-codegen/</guid><description>&lt;p&gt;&lt;code&gt;go:generate&lt;/code&gt; is Go&amp;rsquo;s mechanism for attaching arbitrary code generation commands to your source files. It is not magic — it is a convention that tells &lt;code&gt;go generate&lt;/code&gt; which commands to run and where. When you run &lt;code&gt;go generate ./...&lt;/code&gt;, the tool reads every &lt;code&gt;//go:generate&lt;/code&gt; comment in your source tree and executes the commands they reference. What those commands produce is up to you: &lt;code&gt;String()&lt;/code&gt; methods for enum types, serialization code, mock implementations, database query functions, or anything else you can express as a code generator.&lt;/p&gt;</description></item><item><title>Lesson 8: Packaging and Distributing — GoReleaser, Homebrew, and getting your tool to users</title><link>/post/go/go-cli-distribution/</link><pubDate>Thu, 05 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-cli-distribution/</guid><description>&lt;p&gt;Building a great CLI tool is half the job. The other half is getting it to users without making them compile it from source, navigate a GitHub releases page manually, or run a curl-pipe-to-bash script from an unverified URL. Distribution is where many Go projects stop short: the binary exists, the README says &lt;code&gt;go install&lt;/code&gt;, and that is considered &amp;ldquo;distributed.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;go install&lt;/code&gt; is fine for Go developers. It is not acceptable for operators, system administrators, and end users who reasonably expect &lt;code&gt;brew install&lt;/code&gt; or &lt;code&gt;apt install&lt;/code&gt;. GoReleaser closes this gap by automating the full release pipeline — cross-compiled binaries, checksums, GitHub releases, Homebrew formulas, Debian packages, Docker images — from a single configuration file and one &lt;code&gt;git push --tags&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Lesson 21: Composition Over Inheritance — Small pieces, loosely joined</title><link>/post/go/go-idioms-composition/</link><pubDate>Mon, 02 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-composition/</guid><description>&lt;p&gt;If you&amp;rsquo;ve come from Java or C++, you&amp;rsquo;re probably waiting for Go to show you its inheritance model. Where&amp;rsquo;s the &lt;code&gt;extends&lt;/code&gt; keyword? Where are the base classes? There aren&amp;rsquo;t any — Go made a deliberate choice to leave them out. After writing a few thousand lines of Go, most people agree it was the right call.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Object-oriented languages lean heavily on the &amp;ldquo;is-a&amp;rdquo; relationship. A &lt;code&gt;Dog&lt;/code&gt; is-a &lt;code&gt;Animal&lt;/code&gt;. A &lt;code&gt;Manager&lt;/code&gt; is-a &lt;code&gt;Employee&lt;/code&gt;. This sounds elegant until real-world complexity enters the picture: is a &lt;code&gt;FlyingFish&lt;/code&gt; a &lt;code&gt;Fish&lt;/code&gt; or a &lt;code&gt;Bird&lt;/code&gt;? Now you&amp;rsquo;re in multiple inheritance territory and things get messy fast. And in Go, the instinct to fake it with named fields creates its own noise:&lt;/p&gt;</description></item><item><title>Lesson 8: Linting with golangci-lint — Automate what reviewers shouldn''t waste time on</title><link>/post/go/go-quality-linting/</link><pubDate>Sun, 01 Jun 2025 00:00:00 +0000</pubDate><guid>/post/go/go-quality-linting/</guid><description>&lt;p&gt;I made a rule for myself a few years ago: if I leave a code review comment about something a tool could have caught, I&amp;rsquo;ve wasted both the author&amp;rsquo;s time and mine. A linter can catch unused variables, missing error checks, shadowed variables, inefficient string concatenation, and dozens of other patterns automatically — in seconds, every commit, without reviewer fatigue. The code review should be about design and correctness, not about whether someone forgot to handle an error returned by &lt;code&gt;rows.Close()&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Lesson 6: Mutexes Done Right — The boring tool that actually works</title><link>/post/go/go-concurrency-mutexes/</link><pubDate>Sat, 31 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-mutexes/</guid><description>&lt;p&gt;Channels get the spotlight in Go talks and blog posts — they&amp;rsquo;re the shiny, idiomatic, philosophically interesting tool. Mutexes feel like the C and Java baggage everyone was supposed to leave behind. But here&amp;rsquo;s what I&amp;rsquo;ve learned after several years of writing concurrent Go: mutexes are often the &lt;em&gt;right&lt;/em&gt; tool, especially when you&amp;rsquo;re protecting shared state, and the problems you see in production are almost never &amp;ldquo;we should&amp;rsquo;ve used channels&amp;rdquo; — they&amp;rsquo;re &amp;ldquo;we copied the mutex&amp;rdquo; or &amp;ldquo;we forgot to narrow the critical section.&amp;rdquo; Learn to use mutexes properly and stop apologizing for them.&lt;/p&gt;</description></item><item><title>Lesson 7: context Internals — How cancellation propagates under the hood</title><link>/post/go/go-stdlib-context/</link><pubDate>Thu, 22 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-stdlib-context/</guid><description>&lt;p&gt;&lt;code&gt;context.Context&lt;/code&gt; is the most important type in Go&amp;rsquo;s standard library that most people use by convention without fully understanding. You pass it as the first argument to functions. You check &lt;code&gt;ctx.Done()&lt;/code&gt; in goroutines. You create derived contexts with &lt;code&gt;context.WithTimeout&lt;/code&gt;. It works — until it doesn&amp;rsquo;t, and you have a goroutine that doesn&amp;rsquo;t cancel when the parent context does, or a context that leaks forever because you forgot to call the cancel function.&lt;/p&gt;</description></item><item><title>Lesson 5: sync.WaitGroup — Wait for everyone, then move on</title><link>/post/go/go-concurrency-waitgroup/</link><pubDate>Wed, 21 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-waitgroup/</guid><description>&lt;p&gt;&lt;code&gt;sync.WaitGroup&lt;/code&gt; is one of the first concurrency primitives you reach for in Go, and also one of the first you misuse. The API looks deceptively simple — three methods, twenty minutes of reading, you think you&amp;rsquo;ve got it. Then you hit a negative counter panic in production, or you find out your &lt;code&gt;Wait()&lt;/code&gt; returned while goroutines were still running, and you spend an afternoon learning the rules you thought you already knew.&lt;/p&gt;</description></item><item><title>Lesson 9: Fake Clean Architecture — Layers without purpose are just folders</title><link>/post/go/go-anti-fake-clean-arch/</link><pubDate>Tue, 20 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-fake-clean-arch/</guid><description>&lt;p&gt;I cloned a Go repository that was described to me as a &amp;ldquo;clean architecture&amp;rdquo; implementation. It had six layers: &lt;code&gt;handler&lt;/code&gt;, &lt;code&gt;usecase&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, &lt;code&gt;repository&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, and &lt;code&gt;dto&lt;/code&gt;. Every user-related operation required touching at least four files across four packages. When I added a new field to the user profile, I updated the database schema, the &lt;code&gt;model.User&lt;/code&gt;, the &lt;code&gt;dto.UserDTO&lt;/code&gt;, the &lt;code&gt;repository.UserRepository&lt;/code&gt;, the &lt;code&gt;service.UserService&lt;/code&gt;, the &lt;code&gt;usecase.UserUseCase&lt;/code&gt;, and the &lt;code&gt;handler.UserHandler&lt;/code&gt;. Eight files for one field. The indirection was total, the business logic was nowhere — it was scattered across the layers in thin delegating functions that called the layer below and returned the result.&lt;/p&gt;</description></item><item><title>Lesson 4: The comma ok Idiom — Two returns that save you from panics</title><link>/post/go/go-idioms-comma-ok/</link><pubDate>Mon, 19 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-comma-ok/</guid><description>&lt;p&gt;There are three places in Go where a missing second return value means your program either silently does the wrong thing or blows up entirely: reading from a map with a missing key, asserting a type on an interface, and receiving from a closed channel. The language&amp;rsquo;s answer to all three is the same — a boolean second return that tells you whether the operation actually succeeded. This is the comma-ok idiom, and you&amp;rsquo;ll use it constantly.&lt;/p&gt;</description></item><item><title>Lesson 5: Embedding and Vector Search — Semantic search in Go without Python</title><link>/post/go/go-ai-embeddings/</link><pubDate>Sun, 18 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-ai-embeddings/</guid><description>&lt;p&gt;For a long time, embedding-based semantic search felt like Python territory. The tutorials all pointed to LangChain, FAISS, and numpy. But the actual operations — generate an embedding vector, store it in a database, query for nearest neighbors — map directly onto Go&amp;rsquo;s strengths: clean HTTP client code for the embedding API, &lt;code&gt;pgx&lt;/code&gt; for PostgreSQL with pgvector, and fast concurrent query pipelines. I&amp;rsquo;ve built production semantic search systems entirely in Go and they&amp;rsquo;re fast, maintainable, and don&amp;rsquo;t require a Python sidecar.&lt;/p&gt;</description></item><item><title>Lesson 8: gRPC Basics and Streaming — Protobuf on the wire, types in your code</title><link>/post/go/go-net-grpc/</link><pubDate>Thu, 15 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-net-grpc/</guid><description>&lt;p&gt;My team migrated a set of internal service APIs from JSON over HTTP/1.1 to gRPC roughly two years ago. The motivating factors were type safety across service boundaries, binary serialization that was 5-10x smaller on the wire, and bidirectional streaming that HTTP/1.1 can&amp;rsquo;t do at all. The migration took about a week per service and the performance improvements were immediately visible in our latency percentiles.&lt;/p&gt;
&lt;p&gt;gRPC is a Remote Procedure Call framework that runs over HTTP/2. The interface is defined in Protocol Buffers — a language-neutral schema language — and the gRPC toolchain generates client and server stubs in Go. You call a method; the framework handles serialization, connection management, and streaming.&lt;/p&gt;</description></item><item><title>Lesson 7: Panic, Recover, and When They're Actually Justified — Panic is not error handling</title><link>/post/go/go-errors-panic-recover/</link><pubDate>Tue, 13 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-errors-panic-recover/</guid><description>&lt;p&gt;I&amp;rsquo;ve seen &lt;code&gt;panic(err)&lt;/code&gt; used as error handling more times than I&amp;rsquo;d like to admit — including in codebases I helped build. The reasoning always sounds logical at the time: &amp;ldquo;this error should never happen, and if it does, the program state is corrupt anyway, so why not just panic?&amp;rdquo; The problem is that &amp;ldquo;should never happen&amp;rdquo; is a statement about your expectations, not about reality. And &amp;ldquo;program state is corrupt&amp;rdquo; is almost never true — one request failed, the other thousand are still fine.&lt;/p&gt;</description></item><item><title>Lesson 4: Buffered vs Unbuffered Channels — Buffering hides bugs</title><link>/post/go/go-concurrency-buffered-vs-unbuffered/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-buffered-vs-unbuffered/</guid><description>&lt;p&gt;The first time someone tells you that buffered channels are faster, you believe them — and you spend the next year throwing &lt;code&gt;make(chan T, 100)&lt;/code&gt; at every performance complaint, wondering why your system still deadlocks sometimes, and why those deadlocks disappeared when you bumped the buffer to 200. The buffer wasn&amp;rsquo;t fixing anything. It was delaying the problem. Understanding why requires actually thinking about what buffering changes — not at the performance level, but at the coordination level.&lt;/p&gt;</description></item><item><title>Lesson 9: Config Management — Twelve-factor or twelve headaches</title><link>/post/go/go-api-config/</link><pubDate>Sat, 10 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-api-config/</guid><description>&lt;p&gt;I have seen configuration managed in at least ten different ways across the Go projects I have worked on: hardcoded constants, config structs passed around by pointer, global variables read at startup, YAML files baked into Docker images, environment variables with no validation, and one particularly creative approach involving a shared Google Sheet. Every one of those had the same core problem: the configuration was not treated as a first-class part of the application.&lt;/p&gt;</description></item><item><title>Lesson 8: Designing Interfaces for Libraries — Libraries export interfaces, apps consume them</title><link>/post/go/go-iface-library-design/</link><pubDate>Thu, 08 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-iface-library-design/</guid><description>&lt;p&gt;Writing a library for other Go developers is a fundamentally different design problem than writing application code. In an application, you control every call site. If an interface needs to change, you update all the callers. In a library, your callers are other people, other teams, other binaries you have never seen — and when you change a public interface, you break them all in ways you cannot fix yourself. This constraint forces a discipline that makes library-authored Go interfaces among the most carefully designed in the ecosystem.&lt;/p&gt;</description></item><item><title>Lesson 16: Channels Are for Coordination — Stop using channels as fancy mutexes</title><link>/post/go/go-idioms-channels-coordination/</link><pubDate>Mon, 05 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-channels-coordination/</guid><description>&lt;p&gt;Channels are Go&amp;rsquo;s most recognizable concurrency feature, and also one of the most misused. The moment engineers learn about them, there&amp;rsquo;s a strong temptation to reach for a channel every time two goroutines need to interact. That instinct is wrong about half the time. Channels are for coordination — signaling events, distributing work, collecting results. They are not a universal replacement for shared state.&lt;/p&gt;
&lt;p&gt;Rob Pike&amp;rsquo;s line from his 2012 talk sums it up: &amp;ldquo;Do not communicate by sharing memory; share memory by communicating.&amp;rdquo; That&amp;rsquo;s a guiding philosophy, not an absolute rule.&lt;/p&gt;</description></item><item><title>Lesson 9: Dependency Scanning — govulncheck before you deploy</title><link>/post/go/go-sec-dependency-scanning/</link><pubDate>Mon, 05 May 2025 00:00:00 +0000</pubDate><guid>/post/go/go-sec-dependency-scanning/</guid><description>&lt;p&gt;The security posture of your Go application is not just about the code you write — it is also about the code you import. A typical Go microservice will have dozens of direct and transitive dependencies. Any one of them might have a known vulnerability in the specific version you are using. Unlike the bugs in your own code, these vulnerabilities are publicly catalogued, exploits are often published, and attackers scan for them at scale.&lt;/p&gt;</description></item><item><title>Lesson 3: Channel Ownership Rules — Who closes the channel?</title><link>/post/go/go-concurrency-channel-ownership/</link><pubDate>Fri, 25 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-channel-ownership/</guid><description>&lt;p&gt;At some point, you&amp;rsquo;re going to close a channel twice and the runtime is going to panic with &lt;code&gt;close of closed channel&lt;/code&gt;. Or you&amp;rsquo;re going to close a channel from the wrong goroutine and send a value into it right after, and the runtime is going to panic with &lt;code&gt;send on closed channel&lt;/code&gt;. These aren&amp;rsquo;t subtle race conditions that only show up under load — they&amp;rsquo;re logic errors that exist because nobody in the codebase agreed on who &lt;em&gt;owns&lt;/em&gt; the channel. That agreement is the whole game.&lt;/p&gt;</description></item><item><title>Lesson 8: Capacity Matters — The allocation tax you''re paying without knowing</title><link>/post/go/go-idioms-capacity-matters/</link><pubDate>Mon, 21 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-capacity-matters/</guid><description>&lt;p&gt;There&amp;rsquo;s a one-line fix that will make your hot paths faster, use less memory, and reduce GC pressure. It costs you nothing in readability. Most Go programmers know about it. Far fewer actually do it consistently. The fix is telling Go how big a slice is going to be before you start filling it.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;This is the pattern you reach for by reflex, especially if you&amp;rsquo;ve come from languages where dynamic arrays just grow as needed:&lt;/p&gt;</description></item><item><title>Lesson 6: Error Boundaries Across Layers — Translate at the border, don't leak internals</title><link>/post/go/go-errors-boundaries/</link><pubDate>Sat, 19 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-errors-boundaries/</guid><description>&lt;p&gt;One of the most subtle security issues I&amp;rsquo;ve encountered in Go APIs isn&amp;rsquo;t an authentication bug or a missing authorization check — it&amp;rsquo;s a SQL error message showing up in a JSON response. Something like &lt;code&gt;{&amp;quot;error&amp;quot;: &amp;quot;pq: duplicate key value violates unique constraint \&amp;quot;users_email_key\&amp;quot;&amp;quot;}&lt;/code&gt;. The frontend is now showing your users your database schema. Not great.&lt;/p&gt;
&lt;p&gt;This happens because somewhere between the repository and the HTTP response, someone got lazy with error translation. The raw database error bubbled all the way up and got written directly into the response. Error boundaries exist to prevent exactly this — they&amp;rsquo;re the translation points between layers, where internal implementation details get converted to appropriate external representations.&lt;/p&gt;</description></item><item><title>Lesson 5: Saga Pattern — Distributed transactions without two-phase commit</title><link>/post/go/go-micro-sagas/</link><pubDate>Fri, 18 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-micro-sagas/</guid><description>&lt;p&gt;Two-phase commit is theoretically elegant and operationally painful. I&amp;rsquo;ve seen it cause system-wide deadlocks during network partitions, coordinator failures that required manual database recovery, and deployment coupling so tight that every service had to be upgraded in lockstep. The saga pattern is the alternative — it trades atomicity for availability, compensates for failures with explicit rollback actions, and keeps each service&amp;rsquo;s transactions entirely local. It&amp;rsquo;s the pattern I reach for whenever I need &amp;ldquo;all of this must succeed together&amp;rdquo; across more than one database.&lt;/p&gt;</description></item><item><title>Lesson 8: String Internals — Immutable, backed by bytes, cheaper than you think</title><link>/post/go/go-internals-strings/</link><pubDate>Tue, 15 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-internals-strings/</guid><description>&lt;p&gt;I used to reach for &lt;code&gt;[]byte&lt;/code&gt; over &lt;code&gt;string&lt;/code&gt; in performance-sensitive code, assuming strings were somehow more expensive because &amp;ldquo;immutability must cost something.&amp;rdquo; That intuition was wrong. Understanding what a string actually is — a read-only slice header — corrected my assumptions and simplified a lot of code I had unnecessarily complicated.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Strings in Go are everywhere, and most developers use them without thinking about what they are at the memory level. This leads to a few common mistakes:&lt;/p&gt;</description></item><item><title>Lesson 2: Cancellation with context.Context — Every goroutine needs a kill switch</title><link>/post/go/go-concurrency-context-cancellation/</link><pubDate>Sun, 13 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-context-cancellation/</guid><description>&lt;p&gt;Without &lt;code&gt;context.Context&lt;/code&gt;, you have goroutines running long after the user who triggered them gave up, HTTP connections burning server resources for requests nobody&amp;rsquo;s waiting for, and database queries executing on behalf of clients that disconnected ten seconds ago. Context is the mechanism Go gives you to propagate &amp;ldquo;I changed my mind&amp;rdquo; — and most engineers don&amp;rsquo;t use it until something in production embarrasses them into learning it properly.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s start with the most common mistake — blocking on a slow operation with no timeout:&lt;/p&gt;</description></item><item><title>Lesson 7: Profiling in Containers — pprof works in Kubernetes too</title><link>/post/go/go-deploy-profiling-containers/</link><pubDate>Sat, 12 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-deploy-profiling-containers/</guid><description>&lt;p&gt;The first time I needed to profile a Go service in production, I assumed I&amp;rsquo;d have to deploy a special build, reproduce the problem locally, or use some heavyweight APM product. Then I learned that Go&amp;rsquo;s &lt;code&gt;net/http/pprof&lt;/code&gt; package can serve live profiling data from a running process via HTTP — in a container, in Kubernetes, right now, without redeployment.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pprof&lt;/code&gt; is the built-in Go profiler. It can capture CPU profiles (what functions are consuming CPU time), heap profiles (what&amp;rsquo;s allocated in memory and by whom), goroutine profiles (how many goroutines exist and what they&amp;rsquo;re doing), and block/mutex profiles (where goroutines are waiting). All of this is available as HTTP endpoints that you can scrape with &lt;code&gt;go tool pprof&lt;/code&gt; from your laptop.&lt;/p&gt;</description></item><item><title>Lesson 8: Avoiding Premature Optimization — Measure first, optimize never (usually)</title><link>/post/go/go-perf-premature-optimization/</link><pubDate>Thu, 10 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-perf-premature-optimization/</guid><description>&lt;p&gt;I&amp;rsquo;ve spent time in this series showing you how to make Go programs faster — escape analysis, stack allocation, pre-sizing data structures, zero-copy string handling, benchmarking discipline, pprof profiling, CPU vs memory tradeoffs. Every technique is real and useful. And every single one of them has been misapplied, including by me, when applied before understanding whether they were needed. The last lesson isn&amp;rsquo;t another technique. It&amp;rsquo;s the discipline that makes all the other techniques worth using: measure first, understand where the actual cost is, and optimize only there.&lt;/p&gt;</description></item><item><title>Lesson 6: Avoiding Reflection — Generics and code generation often win</title><link>/post/go/go-reflect-avoiding/</link><pubDate>Tue, 08 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-reflect-avoiding/</guid><description>&lt;p&gt;After spending several lessons on what reflection can do, it is worth turning the question around: when should you specifically choose not to use reflection, even when it would work? The answer is almost always &amp;ldquo;when you have a statically typed alternative,&amp;rdquo; because statically typed code is faster, catches errors at compile time rather than runtime, and is easier for both humans and tools to reason about.&lt;/p&gt;
&lt;p&gt;Go 1.18 added generics, which eliminated one of the most common justifications for reflection: writing algorithms that work on slices, maps, or other containers of unknown element type. Code generation eliminates another: producing type-specific serializers, converters, and mappers that are faster than reflection can ever be. Knowing when to reach for these alternatives instead of reflection is part of writing mature Go.&lt;/p&gt;</description></item><item><title>Lesson 6: Accept Interfaces, Return Structs — Flexibility in, certainty out</title><link>/post/go/go-idioms-accept-interfaces-return-structs/</link><pubDate>Mon, 07 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-idioms-accept-interfaces-return-structs/</guid><description>&lt;p&gt;Here&amp;rsquo;s a mistake I see in almost every Go codebase written by people coming from Java or C#: they accept concrete types everywhere and return interfaces from constructors. It feels &amp;ldquo;enterprise-y&amp;rdquo;. It&amp;rsquo;s actually backwards. The idiomatic Go version is the opposite — accept the smallest interface that does the job, return the richest concrete type you have.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Most of the pain comes from accepting concrete types. Once you lock a function to a specific concrete type, every caller that doesn&amp;rsquo;t have exactly that type is stuck. Tests become integration tests. Swapping implementations requires rewriting functions. And the function itself becomes harder to compose.&lt;/p&gt;</description></item><item><title>Lesson 1: Goroutine Lifecycle Management — Who owns this goroutine?</title><link>/post/go/go-concurrency-goroutine-lifecycle/</link><pubDate>Sun, 06 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-concurrency-goroutine-lifecycle/</guid><description>&lt;p&gt;Nobody tells you that the first goroutine you &amp;ldquo;fire and forget&amp;rdquo; in production is the one that eventually takes down your service at 3 AM. You start it, it runs, and everything looks fine — until requests pile up, memory climbs, and your dashboards turn red because five hundred goroutines are blocked waiting on a channel that&amp;rsquo;ll never receive another value. The problem isn&amp;rsquo;t that goroutines are dangerous. It&amp;rsquo;s that nobody taught you to think about &lt;em&gt;who owns them&lt;/em&gt;.&lt;/p&gt;</description></item><item><title>Lesson 7: Build Flags and ldflags — Inject version info at compile time</title><link>/post/go/go-cli-build-flags/</link><pubDate>Sat, 05 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-cli-build-flags/</guid><description>&lt;p&gt;A CLI tool that cannot tell you what version it is running is a frustrating tool to operate. When something breaks, the first question is &amp;ldquo;which version?&amp;rdquo; Without a proper answer, debugging becomes archaeology. The good news is that Go&amp;rsquo;s build system provides two mechanisms for injecting metadata at compile time — &lt;code&gt;ldflags&lt;/code&gt; for injecting variable values from the shell, and build constraints for including or excluding code based on the build context — and both are straightforward once you understand their syntax.&lt;/p&gt;</description></item><item><title>Lesson 8: Premature Abstraction — Wrong abstraction costs more than duplication</title><link>/post/go/go-anti-premature-abstraction/</link><pubDate>Wed, 02 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-premature-abstraction/</guid><description>&lt;p&gt;There is a principle in software development — &amp;ldquo;Don&amp;rsquo;t Repeat Yourself&amp;rdquo; — that is so widely known it gets abbreviated to DRY and invoked to justify almost any abstraction. The problem is that DRY is a principle about knowledge, not syntax. Two pieces of code that look the same but represent different concepts should stay separate. Two pieces of code that represent the same concept should indeed be unified. Most premature abstractions happen when developers see syntactic similarity and immediately reach for abstraction, before they understand whether the similarity is incidental or fundamental.&lt;/p&gt;</description></item><item><title>Lesson 7: Kill the Utils Package — util.go is where code goes to hide</title><link>/post/go/go-quality-kill-utils/</link><pubDate>Tue, 01 Apr 2025 00:00:00 +0000</pubDate><guid>/post/go/go-quality-kill-utils/</guid><description>&lt;p&gt;Every Go codebase I&amp;rsquo;ve worked on for more than a year has a &lt;code&gt;util&lt;/code&gt; package. Some have a &lt;code&gt;helpers&lt;/code&gt; package. Some have both. I&amp;rsquo;ve seen &lt;code&gt;common&lt;/code&gt;, &lt;code&gt;shared&lt;/code&gt;, &lt;code&gt;misc&lt;/code&gt;, and once, memorably, &lt;code&gt;stuff&lt;/code&gt;. These packages are where code goes when a developer doesn&amp;rsquo;t know where it belongs — which means they&amp;rsquo;re the first place reviewers stop reading carefully, the last place new engineers look when they can&amp;rsquo;t find something, and the primary location of dead code in any codebase over 18 months old.&lt;/p&gt;</description></item><item><title>Lesson 10: Test Architecture — Tests that survive refactoring</title><link>/post/go/go-testing-architecture/</link><pubDate>Tue, 25 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-testing-architecture/</guid><description>&lt;p&gt;Every test suite starts clean. Then the codebase grows, the team grows, deadlines hit, and gradually the tests become the thing you dread touching. You add a feature and a hundred tests break — not because the feature is wrong, but because the tests were coupled to implementation details. You rename a method and spend more time updating tests than writing the feature. Sound familiar? The problem isn&amp;rsquo;t the tests themselves. It&amp;rsquo;s the architecture — the structure, the layering, and the principles that determine whether your test suite stays an asset or becomes a liability.&lt;/p&gt;</description></item><item><title>Lesson 6: sync Package Complete Guide — Mutex, Once, Pool, Map — when to use each</title><link>/post/go/go-stdlib-sync/</link><pubDate>Sat, 22 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-stdlib-sync/</guid><description>&lt;p&gt;The &lt;code&gt;sync&lt;/code&gt; package is where Go&amp;rsquo;s concurrency tools live when channels aren&amp;rsquo;t the right answer. &lt;code&gt;sync.Mutex&lt;/code&gt;, &lt;code&gt;sync.RWMutex&lt;/code&gt;, &lt;code&gt;sync.WaitGroup&lt;/code&gt;, &lt;code&gt;sync.Once&lt;/code&gt;, &lt;code&gt;sync.Pool&lt;/code&gt;, &lt;code&gt;sync.Map&lt;/code&gt; — each one solves a specific problem, and using the wrong one is a reliable way to introduce bugs or performance regressions. I&amp;rsquo;ve misused all of them at various points.&lt;/p&gt;
&lt;p&gt;The general guidance is channels for communication, mutexes for protecting shared state. But within the mutex family, the choice between Mutex, RWMutex, and sync.Map has performance implications that matter in hot paths. And sync.Pool is frequently misunderstood — it&amp;rsquo;s not a general-purpose object pool; it&amp;rsquo;s a GC-aware buffer recycler.&lt;/p&gt;</description></item><item><title>Lesson 7: Outbox Pattern — Reliable events without distributed transactions</title><link>/post/go/go-net-outbox/</link><pubDate>Thu, 20 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-net-outbox/</guid><description>&lt;p&gt;Here&amp;rsquo;s a bug that&amp;rsquo;s bitten almost every distributed system I&amp;rsquo;ve worked on: a service saves a record to the database and then publishes an event to Kafka. The record is saved. The process crashes before the publish. Now the database says the order exists, but the fulfillment service never heard about it. The order sits in limbo forever.&lt;/p&gt;
&lt;p&gt;The naive fix — wrapping both operations in a transaction — doesn&amp;rsquo;t work because Kafka isn&amp;rsquo;t a participant in your database transaction. You can&amp;rsquo;t two-phase commit across Postgres and Kafka without a distributed transaction coordinator, and nobody wants to operate one of those in production.&lt;/p&gt;</description></item><item><title>Lesson 5: Logging vs Returning — Log at the boundary, return everywhere else</title><link>/post/go/go-errors-logging-vs-returning/</link><pubDate>Mon, 17 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-errors-logging-vs-returning/</guid><description>&lt;p&gt;There&amp;rsquo;s a smell I encounter in almost every codebase I&amp;rsquo;ve reviewed — including ones I wrote early in my Go career. Someone discovers that &lt;code&gt;err != nil&lt;/code&gt; and, out of an abundance of caution, they log it right there. Then return it. Then the caller logs it again. Then the middleware logs it a third time. By the time a single failed database query reaches the response, it&amp;rsquo;s appeared in the logs four times with slightly different messages and you can&amp;rsquo;t tell whether four things failed or one thing failed four times.&lt;/p&gt;</description></item><item><title>Lesson 8: Rate Limiting — Say no before you break</title><link>/post/go/go-api-rate-limiting/</link><pubDate>Sat, 15 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-api-rate-limiting/</guid><description>&lt;p&gt;A service I was running had no rate limiting. One night, a script that a client was testing started sending requests in a tight loop — about 4,000 requests per second. The database connection pool saturated within seconds. Other clients started seeing timeouts. By the time I woke up and deployed a fix, we had been degraded for forty minutes. A single &lt;code&gt;429 Too Many Requests&lt;/code&gt; response would have stopped the script cold in seconds.&lt;/p&gt;</description></item><item><title>Lesson 4: Tool Calling Patterns — Letting the LLM invoke your Go functions</title><link>/post/go/go-ai-tool-calling/</link><pubDate>Wed, 12 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-ai-tool-calling/</guid><description>&lt;p&gt;Tool calling is where LLM integrations get genuinely powerful. Without tool calling, you&amp;rsquo;re limited to asking the model to generate text. With tool calling, you can build a conversational interface where the model decides which of your functions to call, calls them, incorporates the results into its reasoning, and decides whether to call more tools or return a final answer. I&amp;rsquo;ve used this to build support assistants that query databases, coding assistants that run test suites, and research tools that fetch live web content — all driven by the model&amp;rsquo;s judgment about which tools to invoke.&lt;/p&gt;</description></item><item><title>Lesson 8: Secure HTTP Defaults — Your production server needs these headers</title><link>/post/go/go-sec-http-defaults/</link><pubDate>Mon, 10 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-sec-http-defaults/</guid><description>&lt;p&gt;When I run the Go HTTP server in the standard library with no configuration, it does a lot of things right — it is fast, it handles HTTP/2, it is well-tested. But it also does a handful of things that are wrong for production: no timeouts, no response headers that protect against common browser-based attacks, and server identification information in responses. These are not bugs in the standard library; they are defaults appropriate for development that need to be changed before you deploy.&lt;/p&gt;</description></item><item><title>Lesson 7: The io.Reader/Writer Ecosystem — The most powerful 2-method interfaces in Go</title><link>/post/go/go-iface-io-ecosystem/</link><pubDate>Sat, 08 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-iface-io-ecosystem/</guid><description>&lt;p&gt;If you want to understand what makes Go interfaces powerful, do not start with your own code. Start with &lt;code&gt;io.Reader&lt;/code&gt;. It is one method — &lt;code&gt;Read(p []byte) (n int, err error)&lt;/code&gt; — and it is the foundation of an entire ecosystem that spans files, network connections, HTTP bodies, compressed streams, encrypted data, buffered reads, pipes, test helpers, and dozens of third-party libraries. Everything that produces bytes implements &lt;code&gt;io.Reader&lt;/code&gt;. Everything that consumes bytes accepts one.&lt;/p&gt;</description></item><item><title>Lesson 7: The Complete Observability Stack — Logs, metrics, traces, profiles — wired together</title><link>/post/go/go-obs-complete-stack/</link><pubDate>Wed, 05 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-obs-complete-stack/</guid><description>&lt;p&gt;We have covered each signal in isolation — structured logs, Prometheus metrics, OpenTelemetry traces, correlation IDs, pprof profiles, and latency distributions. The preceding six lessons described the individual instruments. This one is about wiring them together into a system that actually works during an incident, not just in a demo.&lt;/p&gt;
&lt;p&gt;The goal is this: when something breaks in production, you should be able to answer four questions within five minutes: Is it broken? Who is affected? Where in the system did it break? What is the root cause? Logs, metrics, traces, and profiles each answer one of those questions. The wiring between them — shared trace IDs, consistent service names, deployment markers on dashboards — is what lets you move between signals without losing context.&lt;/p&gt;</description></item><item><title>Lesson 5: What's New in Go 1.25–1.26 — Swiss table maps, weak pointers, and the future</title><link>/post/go/go-modern-latest/</link><pubDate>Sun, 02 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-modern-latest/</guid><description>&lt;p&gt;Every major Go release follows a rhythm: one or two headline language features, a handful of standard library additions, and a runtime improvement you probably do not notice directly but that makes your services run better in aggregate. Go 1.23 and 1.24 brought the iterator protocol, &lt;code&gt;go tool&lt;/code&gt; improvements, and the &lt;code&gt;unique&lt;/code&gt; package. Go 1.25 and 1.26 continued this pattern with Swiss table map internals, a production-ready weak pointer API, and improvements to the toolchain that affect how you build, ship, and profile Go binaries.&lt;/p&gt;</description></item><item><title>Lesson 7: Values Copy vs Share — When Go copies and when it doesn't</title><link>/post/go/go-internals-copy-share/</link><pubDate>Sat, 01 Mar 2025 00:00:00 +0000</pubDate><guid>/post/go/go-internals-copy-share/</guid><description>&lt;p&gt;Early in my Go career I had a bug where I modified a slice inside a function expecting the caller&amp;rsquo;s slice to remain unchanged — and it did. Then I had a different bug where I expected the opposite and the caller&amp;rsquo;s slice &lt;em&gt;was&lt;/em&gt; modified. I had no mental model for predicting which would happen. Building that model is what this lesson is about.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Go passes everything by value. That&amp;rsquo;s the official line, and it&amp;rsquo;s true. But &amp;ldquo;by value&amp;rdquo; means different things for different types:&lt;/p&gt;</description></item><item><title>Lesson 4: Operational vs Domain Errors — Not all errors deserve the same treatment</title><link>/post/go/go-errors-operational-vs-domain/</link><pubDate>Fri, 28 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-errors-operational-vs-domain/</guid><description>&lt;p&gt;One of the more expensive lessons I learned was treating all errors the same. When a database connection drops, you retry. When a user sends an invalid email address, you return a 400 and explain what&amp;rsquo;s wrong. When someone tries to access a resource that doesn&amp;rsquo;t belong to them, you return a 403. These are completely different situations — different causes, different remedies, different communication needs — and collapsing them into a single &lt;code&gt;err != nil&lt;/code&gt; branch produces services that retry permanent failures, expose internal details to users, and log noise that drowns out real alerts.&lt;/p&gt;</description></item><item><title>Lesson 7: go.work and Workspace Mode — Developing multiple modules without replace hacks</title><link>/post/go/go-pkg-workspaces/</link><pubDate>Tue, 25 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-workspaces/</guid><description>&lt;p&gt;Before &lt;code&gt;go.work&lt;/code&gt; existed, developing across multiple local Go modules was genuinely painful. You were working on a library in one directory and an application that depended on it in another. Every time you changed the library, you had to add a &lt;code&gt;replace&lt;/code&gt; directive to the application&amp;rsquo;s &lt;code&gt;go.mod&lt;/code&gt;, run your tests, and then remember — always remember — to remove the &lt;code&gt;replace&lt;/code&gt; before committing. I have seen &lt;code&gt;replace&lt;/code&gt; directives committed to production &lt;code&gt;go.mod&lt;/code&gt; files more times than I would like to admit.&lt;/p&gt;</description></item><item><title>Lesson 7: CPU vs Memory Tradeoffs — Cache it or compute it, pick one</title><link>/post/go/go-perf-cpu-memory/</link><pubDate>Thu, 20 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-perf-cpu-memory/</guid><description>&lt;p&gt;Every performance optimization ultimately makes the same trade: you&amp;rsquo;re giving up memory to gain CPU time, or giving up CPU time to reduce memory usage. There&amp;rsquo;s no free lunch. The skill isn&amp;rsquo;t knowing that the tradeoff exists — it&amp;rsquo;s knowing which side of it you&amp;rsquo;re on, and making the choice consciously rather than accidentally. I&amp;rsquo;ve optimized for memory when the bottleneck was CPU, and optimized for CPU when memory was the problem. Both directions are wrong when you pick them without data.&lt;/p&gt;</description></item><item><title>Lesson 7: Panic as Error Handling — Panic is for bugs, not business logic</title><link>/post/go/go-anti-panic-misuse/</link><pubDate>Tue, 18 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-panic-misuse/</guid><description>&lt;p&gt;I have reviewed Go code from developers who learned other languages where exceptions are the primary error handling mechanism. In Ruby, Python, or Java, you throw an exception and it propagates up the stack until something catches it. In Go, the analogous mechanism — panic — has a very different contract. A panic that is not recovered crashes the entire process. In a web server, that means every in-flight request dies. In a worker process, that means all queued work is dropped.&lt;/p&gt;</description></item><item><title>Lesson 9: Flaky Test Control — A flaky test is worse than no test</title><link>/post/go/go-testing-flaky/</link><pubDate>Sat, 15 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-testing-flaky/</guid><description>&lt;p&gt;A flaky test is a test that sometimes passes and sometimes fails with no code change in between. It sounds like a minor annoyance. It&amp;rsquo;s actually corrosive. Once your team learns that the test suite sometimes fails for &amp;ldquo;no reason,&amp;rdquo; they start merging on red. They start dismissing failures. They lose trust in the test suite as a signal. A single reliably-failing test is informative; a dozen flaky tests teach your team to ignore failures. I&amp;rsquo;ve seen this destroy test suite culture on multiple teams.&lt;/p&gt;</description></item><item><title>Lesson 6: Race Detector in CI — Run -race on every PR or ship bugs</title><link>/post/go/go-deploy-race-ci/</link><pubDate>Wed, 12 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-deploy-race-ci/</guid><description>&lt;p&gt;A data race is a memory safety bug. Two goroutines access the same variable without synchronization, at least one is writing, and the Go memory model makes no guarantees about what happens. In practice: values get corrupted, counters drift, maps panic at runtime. These bugs are intermittent — they appear under load, on fast machines, during deploys — and they&amp;rsquo;re nearly impossible to reproduce deterministically.&lt;/p&gt;
&lt;p&gt;The Go race detector is one of the most powerful debugging tools in any language ecosystem. It instruments every memory access at the compiler level and reports races precisely: the exact goroutine stacks at the moment of the conflicting accesses. But it only helps if you run it. Most teams run it once after a bug report. The right approach is running it on every PR, in CI, before the code is ever merged.&lt;/p&gt;</description></item><item><title>Lesson 5: Validation Frameworks — Reflect once, validate everywhere</title><link>/post/go/go-reflect-validation/</link><pubDate>Mon, 10 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-reflect-validation/</guid><description>&lt;p&gt;Input validation is one of those problems that looks solved until you realize you are writing the same type of check — not nil, min length, valid email format, required when other field is set — dozens of times across dozens of handlers. Centralizing these rules without reflection means either a massive switch statement or hand-writing a validation function for every struct in your application. Reflection makes it possible to declare validation rules once, at the struct, and enforce them automatically everywhere that struct is validated.&lt;/p&gt;</description></item><item><title>Lesson 6: Package Cohesion — Everything in a package should belong together</title><link>/post/go/go-quality-package-cohesion/</link><pubDate>Thu, 06 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-quality-package-cohesion/</guid><description>&lt;p&gt;Go packages are the primary unit of code organization. They determine what&amp;rsquo;s visible to whom, what gets compiled together, and — most importantly — they communicate intent to every engineer who reads the codebase. A package with high cohesion says something true and useful: &amp;ldquo;everything here is about orders&amp;rdquo; or &amp;ldquo;everything here handles HTTP middleware.&amp;rdquo; A package with low cohesion says nothing — it&amp;rsquo;s a filing cabinet where things went when nobody knew where else to put them.&lt;/p&gt;</description></item><item><title>Lesson 6: Embedding Assets — embed.FS puts files inside your binary</title><link>/post/go/go-cli-embed/</link><pubDate>Wed, 05 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-cli-embed/</guid><description>&lt;p&gt;Before Go 1.16, embedding static assets in a Go binary required either a code generation tool that converted files to byte arrays, a third-party library like &lt;code&gt;packr&lt;/code&gt; or &lt;code&gt;statik&lt;/code&gt;, or shipping the files alongside the binary and reading them from disk at runtime. Each approach had real costs: generated code bloated repositories, third-party tools had to be installed separately, and shipping separate files broke the &amp;ldquo;single binary&amp;rdquo; distribution story.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;//go:embed&lt;/code&gt; directive in Go 1.16 solved this cleanly. Files, directories, and whole asset trees can be embedded directly into the binary with a single comment and a variable declaration. Templates, SQL migrations, web assets, default configs — everything your binary needs can travel with it.&lt;/p&gt;</description></item><item><title>Lesson 3: Wrapping Strategy — Every wrap should add context, never noise</title><link>/post/go/go-errors-wrapping-strategy/</link><pubDate>Tue, 04 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-errors-wrapping-strategy/</guid><description>&lt;p&gt;There&amp;rsquo;s a certain kind of error message I&amp;rsquo;ve come to dread in production logs: &lt;code&gt;auth: db: sql: connection refused&lt;/code&gt;. Technically, it contains the full path from the handler down to the socket. Practically, it tells me nothing I couldn&amp;rsquo;t figure out from the stack trace — and it takes ten seconds to parse. That error message is the output of wrapping done wrong: every layer added its name reflexively, without thinking about what the reader actually needs.&lt;/p&gt;</description></item><item><title>Lesson 7: Timeouts and Retries — Your client needs a deadline, your server needs a budget</title><link>/post/go/go-api-timeouts-retries/</link><pubDate>Sat, 01 Feb 2025 00:00:00 +0000</pubDate><guid>/post/go/go-api-timeouts-retries/</guid><description>&lt;p&gt;I once watched a service die in slow motion. An upstream dependency started responding slowly — not failing, just slow. Within minutes, all request goroutines were blocked waiting for the upstream. New requests kept arriving. Goroutines piled up. Memory climbed. Eventually the process was killed by the kernel. The upstream recovered in about thirty seconds. My service was down for twelve minutes. Every second of that outage was caused by the absence of a single line: a timeout.&lt;/p&gt;</description></item><item><title>Lesson 4: Distributed Tracing — Follow the request across 5 services</title><link>/post/go/go-micro-tracing/</link><pubDate>Tue, 28 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-micro-tracing/</guid><description>&lt;p&gt;Debugging a microservices system with only logs is like debugging a multi-threaded program with only print statements — possible, but painful in ways that are entirely avoidable. The first time I had to trace a slow request through six services using log grep, I understood why distributed tracing exists. An hour of log correlation that should have been a 10-second click on a flame chart. Distributed tracing gives you that flame chart.&lt;/p&gt;</description></item><item><title>Lesson 6: Eventual Consistency — Your data will be wrong, temporarily</title><link>/post/go/go-net-eventual-consistency/</link><pubDate>Sat, 25 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-net-eventual-consistency/</guid><description>&lt;p&gt;I used to believe that eventual consistency was an exotic property of distributed databases that I&amp;rsquo;d only encounter at Google scale. Then I added a Redis cache to a simple CRUD service and immediately had a bug where users couldn&amp;rsquo;t see their own updates. Welcome to eventual consistency — it shows up the moment you have two places that store the same data.&lt;/p&gt;
&lt;p&gt;Eventual consistency means that if you stop writing to a system, all replicas will eventually converge to the same value. The word &amp;ldquo;eventually&amp;rdquo; can mean milliseconds or minutes, depending on the system. The hard part isn&amp;rsquo;t the definition — it&amp;rsquo;s designing application code that works correctly even when replicas haven&amp;rsquo;t converged yet.&lt;/p&gt;</description></item><item><title>Lesson 5: os and filepath — Cross-platform file operations that actually work</title><link>/post/go/go-stdlib-os-filepath/</link><pubDate>Wed, 22 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-stdlib-os-filepath/</guid><description>&lt;p&gt;Go&amp;rsquo;s &lt;code&gt;os&lt;/code&gt; and &lt;code&gt;path/filepath&lt;/code&gt; packages are among the most underappreciated in the standard library. Most developers know &lt;code&gt;os.Open&lt;/code&gt;, &lt;code&gt;os.Create&lt;/code&gt;, and &lt;code&gt;os.ReadFile&lt;/code&gt;. Far fewer know &lt;code&gt;os.MkdirAll&lt;/code&gt;, &lt;code&gt;os.CreateTemp&lt;/code&gt;, &lt;code&gt;filepath.WalkDir&lt;/code&gt;, or the difference between &lt;code&gt;path&lt;/code&gt; and &lt;code&gt;path/filepath&lt;/code&gt; — which is the difference between code that works everywhere and code that silently breaks on Windows.&lt;/p&gt;
&lt;p&gt;I maintain a CLI tool that runs on macOS, Linux, and Windows. The file operation bugs I&amp;rsquo;ve shipped have taught me exactly which parts of these packages require care.&lt;/p&gt;</description></item><item><title>Lesson 8: Race-Aware Tests — If it passes without -race, it hasn't passed</title><link>/post/go/go-testing-race-aware/</link><pubDate>Mon, 20 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-testing-race-aware/</guid><description>&lt;p&gt;A test suite that passes without &lt;code&gt;-race&lt;/code&gt; is not a clean bill of health. It&amp;rsquo;s a test suite that hasn&amp;rsquo;t checked one of the most insidious categories of bugs in Go: data races. I&amp;rsquo;ve shipped code that passed every test, passed code review, and then caused memory corruption in production because two goroutines were reading and writing a map concurrently. The race detector would have found it in under a second. We just never ran it.&lt;/p&gt;</description></item><item><title>Lesson 7: JWT Caveats — JWTs are not sessions</title><link>/post/go/go-sec-jwt/</link><pubDate>Sat, 18 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-sec-jwt/</guid><description>&lt;p&gt;JWT has become the default answer to &amp;ldquo;how should I handle authentication tokens?&amp;rdquo; in the Go community. I have shipped JWTs in production and I have also shipped systems that would have been much simpler and more secure with server-side sessions. The point of this lesson is not that JWTs are bad — it is that they carry specific security risks that are easy to overlook, and they solve a specific problem (stateless authentication across services) that not every application actually has.&lt;/p&gt;</description></item><item><title>Lesson 6: Debugging Latency and Leaks — Your p99 is lying to you</title><link>/post/go/go-obs-latency-leaks/</link><pubDate>Wed, 15 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-obs-latency-leaks/</guid><description>&lt;p&gt;We had a service whose p99 latency was 800ms. The p50 was 12ms. The SLA was 200ms at p99. All our dashboards showed the p99 breaching during peak traffic, but the service felt fine to most users. We added caching. We optimized the slow database query. We bumped the CPU allocation. The p99 barely moved.&lt;/p&gt;
&lt;p&gt;Three weeks of tuning later, a colleague asked me to show him the raw histogram buckets. We looked at the actual distribution: 98% of requests were under 15ms. The remaining 2% were all clustered around 900ms, nearly a bimodal distribution. This was not a &amp;ldquo;slow&amp;rdquo; service — it was a service with two distinct response time populations, and the p99 was sampling from the slow population. The fix had nothing to do with the code on the hot path.&lt;/p&gt;</description></item><item><title>Lesson 6: Swallowing Errors — The silent failure that cost us 3 hours</title><link>/post/go/go-anti-swallowing-errors/</link><pubDate>Sun, 12 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-anti-swallowing-errors/</guid><description>&lt;p&gt;We had a deployment where user preferences stopped being saved. New preferences were accepted by the API — the endpoint returned 200 — but nothing was written to the database. After three hours of debugging we found it: a &lt;code&gt;defer rows.Close()&lt;/code&gt; inside a transaction helper that was discarding the error from &lt;code&gt;tx.Commit()&lt;/code&gt;. The commit was failing silently, the defer returned without error, and the handler sent a success response. The only indication anything was wrong was a metrics counter nobody had set up an alert for.&lt;/p&gt;</description></item><item><title>Lesson 2: errors.Is and errors.As — Matching errors through the wrapping chain</title><link>/post/go/go-errors-is-as/</link><pubDate>Thu, 09 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-errors-is-as/</guid><description>&lt;p&gt;Go 1.13 shipped what I consider the most important error-handling improvement the language has had: &lt;code&gt;errors.Is&lt;/code&gt;, &lt;code&gt;errors.As&lt;/code&gt;, and the &lt;code&gt;%w&lt;/code&gt; verb. Before that, wrapping errors with context meant breaking the ability to inspect them. You&amp;rsquo;d wrap with &lt;code&gt;fmt.Errorf(&amp;quot;context: %v&amp;quot;, err)&lt;/code&gt; and then the original error was gone — you could log it, but you couldn&amp;rsquo;t match it. Callers would resort to string matching or just give up and let every error map to a 500.&lt;/p&gt;</description></item><item><title>Lesson 6: Memory Alignment and Struct Padding — Field order affects struct size</title><link>/post/go/go-internals-alignment/</link><pubDate>Wed, 08 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-internals-alignment/</guid><description>&lt;p&gt;I once reviewed a PR where someone had defined a struct with a bool field between two int64 fields, resulting in a 24-byte struct instead of the 17 bytes you might naively calculate. When I pointed it out, the response was &amp;ldquo;the compiler should handle that.&amp;rdquo; It doesn&amp;rsquo;t. Go respects the field order you give it, and it inserts padding to satisfy alignment requirements. Knowing the rules means you write efficient structs by default.&lt;/p&gt;</description></item><item><title>Lesson 6: Testability Without Over-Mocking — Fakes beat mocks every time</title><link>/post/go/go-iface-testability/</link><pubDate>Mon, 06 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-iface-testability/</guid><description>&lt;p&gt;Mock frameworks are a seductive solution to a real problem. You have a dependency — a database, an email service, an HTTP client — and you need your tests to run without it. A mock framework promises to solve this with generated code and assertion APIs. In my experience, what actually happens is that the tests become tightly coupled to implementation rather than behavior, they break whenever you refactor internals, and maintaining the mock library becomes a part-time job.&lt;/p&gt;</description></item><item><title>Lesson 6: pprof Deep Dive — CPU, memory, goroutine — read all three</title><link>/post/go/go-perf-pprof/</link><pubDate>Sun, 05 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-perf-pprof/</guid><description>&lt;p&gt;When I first learned about &lt;code&gt;pprof&lt;/code&gt;, I thought it was a single tool that told you &amp;ldquo;what&amp;rsquo;s slow.&amp;rdquo; It took me an embarrassingly long time to understand that it&amp;rsquo;s actually a family of profilers — CPU, heap, allocs, goroutine, mutex, block — each answering a completely different question. Reading one profile and ignoring the others is like diagnosing engine trouble by only checking the oil. You might find something. You&amp;rsquo;ll definitely miss something. The profilers are designed to work together, and once you start reading all three, performance diagnosis goes from guesswork to diagnosis.&lt;/p&gt;</description></item><item><title>Lesson 6: Dependency Direction — Always depend inward, never outward</title><link>/post/go/go-pkg-dependency-direction/</link><pubDate>Thu, 02 Jan 2025 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-dependency-direction/</guid><description>&lt;p&gt;We fixed cycles in lesson 2. But removing cycles is a necessary condition for good architecture, not a sufficient one. You can have a perfectly cycle-free dependency graph where every arrow points in the wrong direction, and the result is still a system that is painful to test, extend, and reason about.&lt;/p&gt;
&lt;p&gt;Dependency direction is about more than just &amp;ldquo;does A import B.&amp;rdquo; It is about which layer owns the core business rules and which layers are allowed to know about which other layers. Get this wrong and your domain logic ends up tangled with your database driver. Get it right and you can replace your entire HTTP layer, or swap Postgres for a different store, without changing a single line of business logic.&lt;/p&gt;</description></item><item><title>Lesson 3: Streaming Responses — Token-by-token output without buffering the whole response</title><link>/post/go/go-ai-streaming/</link><pubDate>Wed, 18 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-ai-streaming/</guid><description>&lt;p&gt;If you&amp;rsquo;ve ever used a non-streaming LLM endpoint in a user-facing feature, you know the experience it creates: the user submits a question, watches a spinner for 8 seconds, then suddenly gets a wall of text. Streaming changes this completely — the user sees output appearing word by word, which feels responsive and alive even when the total time-to-complete is identical. In Go, streaming LLM responses means reading Server-Sent Events (SSE) from the API and piping them to the client in real time. It&amp;rsquo;s genuinely one of the nicer concurrency patterns I&amp;rsquo;ve implemented.&lt;/p&gt;</description></item><item><title>Lesson 5: CI/CD for Go — GitHub Actions that actually catch bugs</title><link>/post/go/go-deploy-cicd/</link><pubDate>Sun, 15 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-deploy-cicd/</guid><description>&lt;p&gt;I&amp;rsquo;ve set up CI pipelines for Go services about a dozen times, and every iteration taught me something about what the pipeline should actually catch. The first version ran &lt;code&gt;go test ./...&lt;/code&gt; and called it done. That caught compilation errors and test failures, but not data races, not staticcheck warnings, not formatting drift, not license issues, not security vulnerabilities in dependencies. Each of those failure categories has caused a production incident at some point. The current version catches most of them before the PR is merged.&lt;/p&gt;</description></item><item><title>Lesson 1: Sentinel vs Typed Errors — Know your error before you handle it</title><link>/post/go/go-errors-sentinel-vs-typed/</link><pubDate>Sat, 14 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-errors-sentinel-vs-typed/</guid><description>&lt;p&gt;When I first started writing Go seriously, I treated errors like fire: try to avoid them, and when you can&amp;rsquo;t, put them out as fast as possible with an &lt;code&gt;if err != nil { return err }&lt;/code&gt;. It took a few production incidents — and a lot of reading other people&amp;rsquo;s codebases — before I understood that errors aren&amp;rsquo;t just signals. They&amp;rsquo;re data. And how you design that data determines how well your callers can respond to failures.&lt;/p&gt;</description></item><item><title>Lesson 4: Structured Logging with slog — The stdlib logger Go always needed</title><link>/post/go/go-modern-slog/</link><pubDate>Thu, 12 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-modern-slog/</guid><description>&lt;p&gt;I have switched logging libraries in Go more times than I care to admit. Started with the standard &lt;code&gt;log&lt;/code&gt; package — fine for scripts, useless in production because you cannot query plain text logs efficiently. Moved to &lt;code&gt;logrus&lt;/code&gt; because everyone was using it. Switched to &lt;code&gt;zap&lt;/code&gt; when I needed better performance. Considered &lt;code&gt;zerolog&lt;/code&gt; when I wanted allocation-free hot paths. Each migration meant updating every file that imported the old library, convincing teammates, and writing bridge adapters for third-party code that used a different logger.&lt;/p&gt;</description></item><item><title>Lesson 5: Cross-Compilation — Build for Linux from your Mac in one command</title><link>/post/go/go-cli-cross-compile/</link><pubDate>Tue, 10 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-cli-cross-compile/</guid><description>&lt;p&gt;One of Go&amp;rsquo;s most practical superpowers is that cross-compilation is a first-class feature, not an afterthought. Two environment variables — &lt;code&gt;GOOS&lt;/code&gt; and &lt;code&gt;GOARCH&lt;/code&gt; — are all you need to produce a Linux binary from macOS, a Windows executable from Linux, or an ARM binary from an x86 machine. No Docker containers required, no cross-compilation toolchain setup, no linker flags hunting. Just &lt;code&gt;GOOS=linux GOARCH=amd64 go build&lt;/code&gt; and you have a production-ready binary for the target platform.&lt;/p&gt;</description></item><item><title>Lesson 6: Idempotency in APIs — Every POST should be safe to retry</title><link>/post/go/go-api-idempotency/</link><pubDate>Sun, 08 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-api-idempotency/</guid><description>&lt;p&gt;A client sends a payment request. The network times out. The client does not know if the payment was processed. Should it retry? If it retries and the payment already went through, the customer gets charged twice. If it does not retry and the payment failed, the order never ships. Without idempotency, there is no safe answer to this question.&lt;/p&gt;
&lt;p&gt;This is not a theoretical concern. It happens every time a mobile app retries a failed request, every time a message queue delivers a message at least once, every time a load balancer retries on a 503.&lt;/p&gt;</description></item><item><title>Lesson 5: Small Functions Win — If you can''t name it clearly, it does too much</title><link>/post/go/go-quality-small-functions/</link><pubDate>Fri, 06 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-small-functions/</guid><description>&lt;p&gt;There is a simple test I apply to any function I&amp;rsquo;m about to merge: can I name it clearly without using &amp;ldquo;and&amp;rdquo;? If the most honest name for a function is &lt;code&gt;validateAndSaveAndNotifyUser&lt;/code&gt;, the function is three functions pretending to be one. The naming test doesn&amp;rsquo;t lie — it&amp;rsquo;s a direct readout of the function&amp;rsquo;s responsibility. I&amp;rsquo;ve seen this test convince engineers who were unmoved by SOLID principles or Uncle Bob quotes, because it&amp;rsquo;s visceral: if you can&amp;rsquo;t say what the function does in a few words, you already know something is wrong.&lt;/p&gt;</description></item><item><title>Lesson 7: Golden File Testing — Store expected output, compare on run</title><link>/post/go/go-testing-golden-files/</link><pubDate>Thu, 05 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-golden-files/</guid><description>&lt;p&gt;Some functions produce output that&amp;rsquo;s too large or too structured to assert inline. A template renderer, a code generator, a JSON serializer for a deeply nested type, a CLI help text formatter — any of these can produce hundreds of lines of output that you need to verify is correct. Hardcoding that expected output inside the test function creates a wall of string literals. The golden file pattern solves this by storing the expected output in a file, comparing against it at test time, and offering a flag to regenerate it when the output intentionally changes.&lt;/p&gt;</description></item><item><title>Lesson 6: Password Hashing — bcrypt or argon2, nothing else</title><link>/post/go/go-sec-password-hashing/</link><pubDate>Mon, 02 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-sec-password-hashing/</guid><description>&lt;p&gt;Password hashing is one of those topics where the correct answer is short and clear, the wrong answers are numerous and subtle, and developers who confidently implement one of the wrong answers often do not know they did anything wrong until a database is leaked and journalists start writing about it. I have sat in a post-mortem where the words &amp;ldquo;we were using MD5&amp;rdquo; were spoken in a conference room full of very quiet people. That was not my code, but I understood how it happened — MD5 was the &amp;ldquo;hash function&amp;rdquo; the developer knew, and they did not realize it was entirely unsuitable for passwords.&lt;/p&gt;</description></item><item><title>Lesson 5: Profiling in Production — pprof is not just for development</title><link>/post/go/go-obs-profiling/</link><pubDate>Sun, 01 Dec 2024 00:00:00 +0000</pubDate><guid>/post/go/go-obs-profiling/</guid><description>&lt;p&gt;I used to think profiling was something you did when you had a performance problem: reproduce it locally, run pprof, stare at the flame graph, fix the hot path. A fire-fighting tool, not an always-on system.&lt;/p&gt;
&lt;p&gt;Then we had a memory leak in production that we couldn&amp;rsquo;t reproduce locally. The heap grew by about 50 MB per hour under real traffic patterns, causing an OOM every eight hours and a rolling restart across the fleet. Our staging environment used synthetic load that didn&amp;rsquo;t trigger the leak. We had no profile data from when the leak was building — only from after the crash, when the heap had already been cleared.&lt;/p&gt;</description></item><item><title>Lesson 4: Building a Serializer — encoding/json under the hood</title><link>/post/go/go-reflect-serializer/</link><pubDate>Thu, 28 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-reflect-serializer/</guid><description>&lt;p&gt;&lt;code&gt;encoding/json&lt;/code&gt; is Go&amp;rsquo;s most-used package and one of its most instructive implementations. Under the hood it is almost entirely reflection: it inspects struct field types, reads &lt;code&gt;json&lt;/code&gt; tags, handles pointer dereferences, recurses into nested structs, and handles special cases like &lt;code&gt;time.Time&lt;/code&gt; and &lt;code&gt;json.Marshaler&lt;/code&gt; interface implementations. Building a simplified version of a struct serializer from scratch is the best way to understand both how reflection works in practice and why &lt;code&gt;encoding/json&lt;/code&gt; makes the design choices it does.&lt;/p&gt;</description></item><item><title>Lesson 5: Ignoring Context — The cancellation nobody checked</title><link>/post/go/go-anti-ignoring-context/</link><pubDate>Fri, 22 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-ignoring-context/</guid><description>&lt;p&gt;I spent a Tuesday afternoon tracking down why our service continued doing expensive database work after clients had long since disconnected. An HTTP client with a 3-second timeout would cancel the request, but our handler would still run three downstream database queries that together took 8 seconds. The handler was checking for errors but never checking the context. By the time it finished, the client had retried twice, and we were now running three copies of the same 8-second work simultaneously.&lt;/p&gt;</description></item><item><title>Lesson 2: gRPC-Based Plugin Architecture — How Terraform and Vault do plugins</title><link>/post/go/go-plugins-grpc/</link><pubDate>Thu, 21 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-plugins-grpc/</guid><description>&lt;p&gt;After I understood how &lt;code&gt;hashicorp/go-plugin&lt;/code&gt; worked with net/rpc, the next question was obvious: how does Terraform manage hundreds of community-contributed providers, written by different teams, evolving on different schedules, sometimes in languages other than Go? The answer is gRPC. Terraform&amp;rsquo;s provider protocol is a protobuf schema, communicated over the same subprocess-plus-RPC architecture from Lesson 1 — but with gRPC instead of net/rpc. That substitution buys you schema evolution, multi-language support, streaming, and a strongly-typed IDL. This lesson shows you how to build a plugin system using that pattern.&lt;/p&gt;</description></item><item><title>Lesson 5: Message Queues in Go — NATS, RabbitMQ, Kafka — pick your tradeoff</title><link>/post/go/go-net-message-queues/</link><pubDate>Wed, 20 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-net-message-queues/</guid><description>&lt;p&gt;I&amp;rsquo;ve integrated all three of these systems in production Go services, and the question I get most often is: &amp;ldquo;Which one should I use?&amp;rdquo; The honest answer is that it depends on what guarantee you actually need — and most engineers pick based on familiarity or hype rather than requirements. NATS, RabbitMQ, and Kafka solve meaningfully different problems, and choosing the wrong one creates operational pain that no amount of clever application code can fix.&lt;/p&gt;</description></item><item><title>Lesson 4: time Package Gotchas — Timezones, monotonic clocks, and the bug in your cron</title><link>/post/go/go-stdlib-time/</link><pubDate>Mon, 18 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-stdlib-time/</guid><description>&lt;p&gt;The &lt;code&gt;time&lt;/code&gt; package looks straightforward right up until the moment a bug report arrives saying &amp;ldquo;the nightly job didn&amp;rsquo;t run last Sunday.&amp;rdquo; It never runs on the Sunday when daylight saving time ends. Because that Sunday has 25 hours, and the cron expression fires twice at the ambiguous time, and the second firing is skipped because the code thinks it already ran. I&amp;rsquo;ve debugged this exact bug, and the root cause is always the same: someone — usually me — assumed time is simpler than it is.&lt;/p&gt;</description></item><item><title>Lesson 8: When Duplication Is Better Than Abstraction — Bad abstraction costs more than repeated code</title><link>/post/go/go-generics-duplication-vs-abstraction/</link><pubDate>Sun, 17 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-generics-duplication-vs-abstraction/</guid><description>&lt;p&gt;I want to end this course with the idea I wish I&amp;rsquo;d understood at the start — not just intellectually, but in my gut. It&amp;rsquo;s this: &lt;strong&gt;duplication is a problem you can see. Bad abstraction is a problem you can&amp;rsquo;t see until it&amp;rsquo;s too late.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Duplicated code is visible. You grep for it, you find it, you fix it. It&amp;rsquo;s annoying, but it&amp;rsquo;s honest. Bad abstraction hides behind clean-looking code. It&amp;rsquo;s the function that requires fifteen minutes of context to understand, the type parameter that nobody knows how to satisfy, the constraint that&amp;rsquo;s technically correct but practically useless. It costs you in onboarding, in debugging, in every change that touches it for the rest of the codebase&amp;rsquo;s life.&lt;/p&gt;</description></item><item><title>Lesson 5: GC Behavior and Tuning — GOGC and GOMEMLIMIT changed the game</title><link>/post/go/go-internals-gc/</link><pubDate>Fri, 15 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-internals-gc/</guid><description>&lt;p&gt;For years, tuning Go&amp;rsquo;s GC meant tweaking &lt;code&gt;GOGC&lt;/code&gt; and hoping for the best. I operated on vibes and guesswork. Then Go 1.19 introduced &lt;code&gt;GOMEMLIMIT&lt;/code&gt; — a hard memory limit that fundamentally changed how I reason about GC tuning. Suddenly I had a second axis of control that actually matched how production memory is constrained. This lesson is the mental model I wish I had from day one.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Go uses a concurrent, tri-color mark-and-sweep garbage collector. &amp;ldquo;Concurrent&amp;rdquo; means most of the GC work happens while your program is running, not in stop-the-world pauses. This is generally excellent, but the GC needs to be triggered somehow, and the default trigger can be surprising.&lt;/p&gt;</description></item><item><title>Lesson 5: Composition with Embedding — Small interfaces compose into powerful contracts</title><link>/post/go/go-iface-composition/</link><pubDate>Tue, 12 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-iface-composition/</guid><description>&lt;p&gt;One of the things that surprised me most about Go when I came from Python was that Go has no inheritance. No base classes, no method overriding, no type hierarchies. What it has instead is embedding — the ability to include one type inside another so that the outer type promotes the inner type&amp;rsquo;s methods. Combined with interface composition, this gives you something more flexible than inheritance: you can build complex behaviors from simple, testable pieces without the fragility that comes from deep class trees.&lt;/p&gt;</description></item><item><title>Lesson 5: Benchmarking Done Right — testing.B is not what you think</title><link>/post/go/go-perf-benchmarking/</link><pubDate>Sun, 10 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-perf-benchmarking/</guid><description>&lt;p&gt;Writing a Go benchmark feels simple. You drop &lt;code&gt;Benchmark&lt;/code&gt; in front of a function name, loop from 0 to &lt;code&gt;b.N&lt;/code&gt;, run &lt;code&gt;go test -bench=.&lt;/code&gt;, and get a number. The number feels authoritative. I spent about a year trusting benchmark numbers that were wrong — not wrong because of bugs, but wrong because of how the benchmark was written. The Go benchmark framework is excellent, but it has sharp edges that will mislead you until you learn to see them.&lt;/p&gt;</description></item><item><title>Lesson 5: Monolith vs Multi-Module — One go.mod or many? It depends.</title><link>/post/go/go-pkg-mono-vs-multi/</link><pubDate>Fri, 08 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-mono-vs-multi/</guid><description>&lt;p&gt;When I first started structuring larger Go projects, I defaulted to what felt natural: one repository, one &lt;code&gt;go.mod&lt;/code&gt;. It worked for a long time. Then I joined a project with four services in a single repo, each with different dependency requirements, and suddenly the single &lt;code&gt;go.mod&lt;/code&gt; was pulling in every dependency of every service for every build. Tests for the email service were slow because the &lt;code&gt;go test&lt;/code&gt; run was loading the ML inference library needed only by the recommendation service. That was when I started thinking seriously about module boundaries, not just package boundaries.&lt;/p&gt;</description></item><item><title>Lesson 3: Service Discovery — Finding services without hardcoding URLs</title><link>/post/go/go-micro-discovery/</link><pubDate>Wed, 06 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-micro-discovery/</guid><description>&lt;p&gt;When I first started building Go microservices, every service had a config file with a list of URLs. &lt;code&gt;order-service-url: http://10.0.1.42:8080&lt;/code&gt;. That worked fine until the service moved, scaled out, or the IP changed during a deployment. Then the deployment would fail, someone would update the config, redeploy, and we&amp;rsquo;d write a Jira ticket to &amp;ldquo;fix the discovery mechanism eventually.&amp;rdquo; Service discovery is that fix — it&amp;rsquo;s how services find each other without hardcoding network locations.&lt;/p&gt;</description></item><item><title>Lesson 6: Mocking Alternatives — Interfaces over mocks, always</title><link>/post/go/go-testing-mocking/</link><pubDate>Tue, 05 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-mocking/</guid><description>&lt;p&gt;Mocking has a bad reputation in the Go community, and some of it is deserved. Not because mocks are inherently wrong, but because the way most people use them — generating verbose stubs from interfaces, asserting on call counts, verifying argument order — produces tests that are coupled to implementation details rather than behaviour. Refactor the internals of a function without changing its public contract, and suddenly half your mocks break. That&amp;rsquo;s not a test problem. That&amp;rsquo;s a mock-as-test-double problem.&lt;/p&gt;</description></item><item><title>Lesson 4: Config Injection — Environment variables, flags, files — in that order</title><link>/post/go/go-deploy-config-injection/</link><pubDate>Sat, 02 Nov 2024 00:00:00 +0000</pubDate><guid>/post/go/go-deploy-config-injection/</guid><description>&lt;p&gt;Configuration management is one of those topics that seems solved until you maintain a service that runs in four different environments, has twenty configurable parameters, and needs its secrets rotated without a redeployment. I&amp;rsquo;ve gone through several evolutionary stages on this: hardcoded values (the naive phase), a single &lt;code&gt;config.yaml&lt;/code&gt; file, then twelve &lt;code&gt;config-{env}.yaml&lt;/code&gt; files, and finally landing on what the 12-factor app methodology describes — environment variables as the source of truth for deployment-specific configuration.&lt;/p&gt;</description></item><item><title>Lesson 4: Signal Handling — Catch SIGTERM or lose your work</title><link>/post/go/go-cli-signals/</link><pubDate>Wed, 30 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-cli-signals/</guid><description>&lt;p&gt;Most CLI tools work perfectly on the happy path and fail silently on the unhappy one. The user presses Ctrl-C, the OS sends SIGINT, and the process dies immediately — leaving a temp file half-written, a database connection open, or a progress bar frozen mid-operation. The problem is invisible because most of the time nobody looks at what gets left behind. Until a deployment script depends on that temp file being complete, or a database hits its connection limit, or a batch job loses six hours of progress because the container was terminated between checkpoints.&lt;/p&gt;</description></item><item><title>Lesson 5: Pagination Done Right — Offset pagination breaks at scale</title><link>/post/go/go-api-pagination/</link><pubDate>Mon, 28 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-api-pagination/</guid><description>&lt;p&gt;I have shipped offset-based pagination more times than I would like to admit. It looks clean, it is trivially easy to implement, and it works perfectly — until the table reaches about 100,000 rows. Then it starts to slow down, and by a million rows it is actively harmful. I had a support dashboard that froze every time someone clicked to page 47. That was the moment I stopped using &lt;code&gt;OFFSET&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Lesson 7: Refactoring Concrete to Generic — Start concrete, extract when proven</title><link>/post/go/go-generics-refactoring/</link><pubDate>Fri, 25 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-generics-refactoring/</guid><description>&lt;p&gt;The advice &amp;ldquo;start concrete, extract when proven&amp;rdquo; is easy to say and surprisingly hard to follow when you&amp;rsquo;re in the middle of writing the third version of the same function. The temptation to generalize early is real. I&amp;rsquo;ve felt it. Most engineers who care about clean code have felt it.&lt;/p&gt;
&lt;p&gt;But the cost of premature abstraction is higher than the cost of temporary duplication. A duplicated function can be removed with a search-and-replace. A bad abstraction gets built around, extended in wrong directions, and defended by sunk-cost thinking. This lesson is about how to do the refactoring correctly — waiting until the pattern is proven, then extracting it cleanly.&lt;/p&gt;</description></item><item><title>Lesson 2: CGo Performance and Pitfalls — The hidden cost of crossing the boundary</title><link>/post/go/go-cgo-performance/</link><pubDate>Thu, 24 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-cgo-performance/</guid><description>&lt;p&gt;When I profiled a service that was spending 40% of its time in cgo calls, I thought I was measuring the C library. I was not. I was measuring the overhead of &lt;em&gt;getting to&lt;/em&gt; the C library. The actual C work was fast. What was slow was the goroutine-to-OS-thread transition, the stack switching, and the runtime bookkeeping that happens every single time Go code crosses the C boundary. Understanding this overhead is what separates cgo code that runs fine from cgo code that becomes a bottleneck.&lt;/p&gt;</description></item><item><title>Lesson 5: Auth Middleware — Authentication is not authorization</title><link>/post/go/go-sec-auth-middleware/</link><pubDate>Tue, 22 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-sec-auth-middleware/</guid><description>&lt;p&gt;A few years ago I audited a Go API where every endpoint was protected by an authentication middleware. The middleware checked for a valid JWT, extracted the user ID, and set it in the request context. The developer was proud of it — every route was secured. The problem was that the product had a concept of &amp;ldquo;organizations&amp;rdquo; — users belonged to organizations — and the API let you fetch any organization&amp;rsquo;s data as long as you were authenticated. The authentication was solid. The authorization was completely absent.&lt;/p&gt;</description></item><item><title>Lesson 4: Code Review Heuristics — What to look for in a Go PR</title><link>/post/go/go-quality-code-review/</link><pubDate>Sun, 20 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-code-review/</guid><description>&lt;p&gt;A good code review is not a diff-reading exercise. It&amp;rsquo;s a transfer of understanding — the reviewer asks &amp;ldquo;do I understand what this code does, why it does it, and what it doesn&amp;rsquo;t do?&amp;rdquo; If the answer to any of those is no, that&amp;rsquo;s a comment, not a nitpick. I&amp;rsquo;ve done hundreds of Go reviews and the feedback I give clusters into the same ten or fifteen patterns so reliably that I eventually wrote them down as a checklist. This lesson is that checklist, with examples.&lt;/p&gt;</description></item><item><title>Lesson 4: Correlation IDs — Connect the logs to the trace to the user</title><link>/post/go/go-obs-correlation-ids/</link><pubDate>Fri, 18 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-obs-correlation-ids/</guid><description>&lt;p&gt;A support ticket lands: &amp;ldquo;User 8842 says their order failed at 2:47 PM yesterday.&amp;rdquo; You open your log aggregator. You search for &lt;code&gt;user_id = 8842&lt;/code&gt;. You get 4,000 log lines — the user made 80 requests that afternoon. You filter to the 2:43–2:51 PM window. You get 300 lines. They interleave with log lines from 12 concurrent requests from other users because your log output is not partitioned by request. The error message, when you find it, says &lt;code&gt;internal server error&lt;/code&gt;. No stack trace, no underlying cause, no request that produced it.&lt;/p&gt;</description></item><item><title>Lesson 4: Channel Misuse — You used a channel where a mutex would do</title><link>/post/go/go-anti-channel-misuse/</link><pubDate>Tue, 15 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-channel-misuse/</guid><description>&lt;p&gt;Go&amp;rsquo;s channels are genuinely great. They make certain concurrent programming patterns — pipelines, fan-out, fan-in, worker pools — natural and readable. Because they are idiomatic and distinctly Go-like, there is a tendency among Go developers to reach for them first whenever concurrency is involved. The result is code that uses channels to protect shared state, which is what mutexes are for, or code that passes one value through a channel with ceremony that could be replaced by a function call.&lt;/p&gt;</description></item><item><title>Lesson 3: Loop Variable Fix — The Go 1.22 change that fixed a decade of bugs</title><link>/post/go/go-modern-loop-var/</link><pubDate>Sun, 13 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-modern-loop-var/</guid><description>&lt;p&gt;If you have written Go for more than a few weeks, you have hit this bug. Or you have reviewed code that had it and caught it. Or — if you were unlucky — you shipped it to production and spent an hour staring at a data race report wondering what on earth was happening. The loop variable capture bug was so common that it was essentially a Go rite of passage. Every Go tutorial mentioned it. Every linter had a rule for it. And for a decade, the answer was always &amp;ldquo;just copy the variable inside the loop.&amp;rdquo;&lt;/p&gt;</description></item><item><title>Lesson 4: Connection Pooling — One connection per request is a performance bug</title><link>/post/go/go-net-conn-pooling/</link><pubDate>Sat, 12 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-net-conn-pooling/</guid><description>&lt;p&gt;The first time I benchmarked a Go service against a database, the numbers were embarrassing. Five hundred requests per second, each one taking 30 milliseconds to execute a trivially simple query. The query itself took 2 milliseconds on the database server. The other 28 milliseconds were TCP handshake plus TLS plus PostgreSQL authentication — repeated for every single request because I had no connection pool.&lt;/p&gt;
&lt;p&gt;Connection pooling is one of those topics that feels like an advanced optimization until you discover that nearly every database driver in Go already pools connections by default — you just have to configure the pool instead of leaving it at its default settings, which are almost always wrong for your workload.&lt;/p&gt;</description></item><item><title>Lesson 5: Database Testing with Testcontainers — Mock the DB and you mock the truth</title><link>/post/go/go-testing-database/</link><pubDate>Thu, 10 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-database/</guid><description>&lt;p&gt;I used to mock databases. I had a clean &lt;code&gt;Store&lt;/code&gt; interface, a &lt;code&gt;MockStore&lt;/code&gt; implementation for tests, and 100% coverage on my service layer. I felt good about it. Then we migrated from MySQL to PostgreSQL and discovered that a dozen subtle behaviours we&amp;rsquo;d been mocking around were wrong — &lt;code&gt;UPSERT&lt;/code&gt; semantics, NULL handling in &lt;code&gt;GROUP BY&lt;/code&gt;, timestamp precision, transaction isolation differences. The mock had been lying to us for months. Testcontainers fixed that.&lt;/p&gt;</description></item><item><title>Lesson 3: Struct Tags — Metadata your compiler ignores but your framework reads</title><link>/post/go/go-reflect-struct-tags/</link><pubDate>Tue, 08 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-reflect-struct-tags/</guid><description>&lt;p&gt;Struct tags are one of Go&amp;rsquo;s most practically useful metaprogramming tools and one of its least formally documented features. The syntax is simple — a raw string literal after the field type in a struct — but the convention built on top of it powers nearly every serialization library, ORM, validator, and configuration loader in the ecosystem. Understanding how tags work at the reflection level demystifies why &lt;code&gt;encoding/json&lt;/code&gt; knows to use &lt;code&gt;omitempty&lt;/code&gt;, why &lt;code&gt;database/sql&lt;/code&gt; scanners can map column names to struct fields, and how you can build your own tag-driven behavior.&lt;/p&gt;</description></item><item><title>Lesson 2: LLM API Clients — Calling Claude, GPT, and Groq from Go</title><link>/post/go/go-ai-llm-clients/</link><pubDate>Sun, 06 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-ai-llm-clients/</guid><description>&lt;p&gt;Most Go developers approach LLM APIs the same way they approach any REST API — write an HTTP client, handle errors, parse JSON. That instinct is correct, but LLM APIs have a few characteristics that require specific handling: they&amp;rsquo;re slow (seconds, not milliseconds), they have complex nested response structures, they support streaming, and the model selection and token management have real cost implications. This lesson is about building Go clients that handle all of this properly.&lt;/p&gt;</description></item><item><title>Lesson 4: Escape Analysis Deep Dive — The compiler decides where your data lives</title><link>/post/go/go-internals-escape-analysis/</link><pubDate>Sat, 05 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-internals-escape-analysis/</guid><description>&lt;p&gt;I used to assume that every time I wrote &lt;code&gt;&amp;amp;someStruct{}&lt;/code&gt; or returned a pointer from a function, I was creating a heap allocation. I was wrong — and the wrongness mattered for how I was designing APIs. After learning about escape analysis, I stopped guessing and started &lt;em&gt;asking the compiler&lt;/em&gt; directly. This changed how I write Go.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Go has two places to put data: the &lt;strong&gt;stack&lt;/strong&gt; and the &lt;strong&gt;heap&lt;/strong&gt;. Stack allocations are cheap — they&amp;rsquo;re just a pointer bump, and the stack frame is reclaimed automatically when the function returns. Heap allocations go through the memory allocator, require garbage collection, and have non-trivial overhead.&lt;/p&gt;</description></item><item><title>Lesson 6: Real-World Examples — Generics that survived code review</title><link>/post/go/go-generics-real-world/</link><pubDate>Wed, 02 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-generics-real-world/</guid><description>&lt;p&gt;Lessons 1 through 5 were mostly about principles. This one is about code. Specifically, the five generic implementations I&amp;rsquo;ve used in real production systems that my teammates didn&amp;rsquo;t complain about. Each one passed code review, made it into production, and held up over time.&lt;/p&gt;
&lt;p&gt;The pattern across all of them is consistent: the algorithm is identical regardless of the type, the duplication without generics would have been mechanical and ongoing, and the generic version is genuinely easier to read than the alternative.&lt;/p&gt;</description></item><item><title>Lesson 4: Returning Concrete Types — Give callers the real thing</title><link>/post/go/go-iface-return-concrete/</link><pubDate>Tue, 01 Oct 2024 00:00:00 +0000</pubDate><guid>/post/go/go-iface-return-concrete/</guid><description>&lt;p&gt;There is a piece of Go advice that sounds almost too obvious to state, and yet I see it violated in production codebases every week: return concrete types from your functions. Not interfaces. Not &lt;code&gt;interface{}&lt;/code&gt;. The actual, named, exported struct that your function produces.&lt;/p&gt;
&lt;p&gt;The reason this feels controversial is that it conflicts with object-oriented instincts. In Java and C#, returning an interface from a factory is considered good practice — it hides the implementation detail and &amp;ldquo;programs to an interface.&amp;rdquo; In Go, that instinct leads to information loss, surprise type assertions, and constructors that promise less than they deliver.&lt;/p&gt;</description></item><item><title>Lesson 3: encoding/json Beyond Basics — Custom marshalers, streaming, and the traps</title><link>/post/go/go-stdlib-json/</link><pubDate>Mon, 30 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-stdlib-json/</guid><description>&lt;p&gt;&lt;code&gt;encoding/json&lt;/code&gt; is one of the first packages every Go developer uses and one of the last they fully understand. The basic &lt;code&gt;json.Marshal&lt;/code&gt; / &lt;code&gt;json.Unmarshal&lt;/code&gt; API is approachable. But the package contains a whole layer of capabilities — custom marshaling, streaming decoders, &lt;code&gt;json.RawMessage&lt;/code&gt; for deferred parsing, interface-type fields, and a surprising number of edge cases — that separate the code that works in demos from the code that works in production with adversarial inputs.&lt;/p&gt;</description></item><item><title>Lesson 4: Interface Placement — Define interfaces where they're used, not where they're implemented</title><link>/post/go/go-pkg-interface-placement/</link><pubDate>Sat, 28 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-interface-placement/</guid><description>&lt;p&gt;This is the Go idiom that surprises people coming from Java or C# the most. In those languages, you define an interface in the same place as (or even before) the implementation, and consumers import the interface. Go&amp;rsquo;s approach is the exact opposite, and it takes a while to internalize why. Once it clicks, it changes how you think about dependencies fundamentally.&lt;/p&gt;
&lt;p&gt;The rule is: define an interface in the package that needs it, not in the package that satisfies it. It sounds backwards. It is not. It is one of the most powerful design decisions the language enables.&lt;/p&gt;</description></item><item><title>Lesson 4: String and Byte Conversions — The copy nobody sees</title><link>/post/go/go-perf-string-bytes/</link><pubDate>Wed, 25 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-perf-string-bytes/</guid><description>&lt;p&gt;Go strings are immutable. Byte slices are mutable. Converting between them requires copying the data — every time, without exception, unless you use unsafe tricks you almost certainly shouldn&amp;rsquo;t. That sounds like a minor footnote, but it becomes a significant issue the moment you start handling large volumes of text: parsing HTTP requests, processing log lines, building JSON responses. I&amp;rsquo;ve watched a single &lt;code&gt;string(b)&lt;/code&gt; call inside a tight loop add measurable latency to a production API, and the fix was two lines of code once I knew what to look for.&lt;/p&gt;</description></item><item><title>Lesson 3: Health Checks — Readiness vs liveness, and why both matter</title><link>/post/go/go-deploy-health-checks/</link><pubDate>Sun, 22 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-deploy-health-checks/</guid><description>&lt;p&gt;I&amp;rsquo;ve seen two types of health check implementations in production: the ones that always return 200 OK, and the ones that actually check something. The first kind is theater — Kubernetes thinks the pod is healthy and routes traffic to it, but the pod is actually deadlocked or its database connection pool is exhausted. The second kind is what saves you at 3am.&lt;/p&gt;
&lt;p&gt;Health checks are Kubernetes&amp;rsquo;s mechanism for deciding when a pod is ready to receive traffic and when it needs to be restarted. Get them right and Kubernetes becomes a genuinely reliable self-healing system. Get them wrong and you have an elaborate system that sends traffic to broken pods and restarts healthy ones.&lt;/p&gt;</description></item><item><title>Lesson 4: Error Responses — Your API errors are your documentation</title><link>/post/go/go-api-error-responses/</link><pubDate>Fri, 20 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-api-error-responses/</guid><description>&lt;p&gt;I once integrated with an API that returned HTTP 200 for everything — successes and failures alike. The actual outcome was buried in a &lt;code&gt;status&lt;/code&gt; field inside the JSON body. To know whether a request worked, you had to parse the response, check &lt;code&gt;status&lt;/code&gt;, then switch on a string value that was inconsistently named across endpoints. Integrating with that API felt like defusing a bomb in the dark.&lt;/p&gt;
&lt;p&gt;How you design error responses is not a detail. It is part of your public interface.&lt;/p&gt;</description></item><item><title>Lesson 4: SSRF and Injection — The URL your user gave you might be localhost</title><link>/post/go/go-sec-ssrf-injection/</link><pubDate>Wed, 18 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-sec-ssrf-injection/</guid><description>&lt;p&gt;I reviewed a Go service that let users provide a &amp;ldquo;webhook URL&amp;rdquo; — we would call that URL when their account had a notification. The service fetched a preview of the URL to display in the UI. The developer who built it figured that since it only fetched the URL and never executed what was returned, it was safe. They had not considered that &lt;code&gt;http://169.254.169.254/latest/meta-data/iam/security-credentials/&lt;/code&gt; is also a URL.&lt;/p&gt;
&lt;p&gt;SSRF — Server-Side Request Forgery — is the class of vulnerability where an attacker tricks your server into making an HTTP request to a target of their choosing, typically to access internal services that are not exposed to the internet. AWS instance metadata, Redis, internal Kubernetes API servers, admin dashboards, other services in your VPC — all are reachable from a server that blindly follows user-supplied URLs.&lt;/p&gt;</description></item><item><title>Lesson 3: Distributed Tracing with OpenTelemetry — Follow the request across services</title><link>/post/go/go-obs-tracing/</link><pubDate>Sun, 15 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-obs-tracing/</guid><description>&lt;p&gt;We had an incident where checkout was timing out intermittently. The logs showed the API gateway receiving the request and returning a 504 after 30 seconds. The payment service logged nothing. The inventory service logged nothing. Something was hanging somewhere in the middle, and we had no way to see where.&lt;/p&gt;
&lt;p&gt;I spent four hours bisecting the call graph by adding temporary log lines, redeploying, and re-triggering the error. We eventually found a database query in the inventory service that was waiting on a lock — a lock held by a background job nobody had thought to instrument. Logs told me what each service did in isolation. They told me nothing about the shape of a single request as it flowed across all of them.&lt;/p&gt;</description></item><item><title>Lesson 3: File I/O Patterns — Read, write, stream without loading everything into memory</title><link>/post/go/go-cli-file-io/</link><pubDate>Thu, 12 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-cli-file-io/</guid><description>&lt;p&gt;CLI tools spend most of their life doing file I/O. Reading config files, processing log dumps, writing output, transforming data from stdin to stdout — it all comes down to bytes moving through your program. The difference between a CLI tool that handles 100MB files gracefully and one that runs out of memory on large inputs is almost always whether you read everything into memory or stream it.&lt;/p&gt;
&lt;p&gt;Go&amp;rsquo;s standard library gives you everything you need to stream data efficiently. The key is knowing which functions to reach for and which ones to avoid when the input is large or unknown in size.&lt;/p&gt;</description></item><item><title>Lesson 2: Inter-Service Communication — HTTP, gRPC, or events? It depends on the coupling.</title><link>/post/go/go-micro-communication/</link><pubDate>Tue, 10 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-micro-communication/</guid><description>&lt;p&gt;Every time I&amp;rsquo;ve seen a team choose their inter-service communication protocol by default — &amp;ldquo;we&amp;rsquo;ll use REST for everything&amp;rdquo; or &amp;ldquo;we&amp;rsquo;re going gRPC-native&amp;rdquo; — they&amp;rsquo;ve ended up with a transport mechanism that fights against some of their use cases. The choice between HTTP, gRPC, and event-driven messaging is a question about coupling: how tightly do these services need to be synchronized? The answer to that question selects your transport, not the other way around.&lt;/p&gt;</description></item><item><title>Lesson 5: Anti-Patterns — Just because you can doesn't mean you should</title><link>/post/go/go-generics-anti-patterns/</link><pubDate>Sun, 08 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-generics-anti-patterns/</guid><description>&lt;p&gt;Every powerful tool has a failure mode that looks like success. With generics, the failure mode is this: you write something that compiles, works correctly, and is impressively abstract — but your teammates can&amp;rsquo;t read it, can&amp;rsquo;t debug it, and quietly work around it. I&amp;rsquo;ve written code like that. It felt clever in the moment. It was a problem in practice.&lt;/p&gt;
&lt;p&gt;This lesson is about the patterns that sound good and turn out badly. I&amp;rsquo;m calling them out explicitly because they&amp;rsquo;re seductive — especially if you&amp;rsquo;ve spent time in Haskell or Scala and you know what generic abstractions &lt;em&gt;can&lt;/em&gt; look like.&lt;/p&gt;</description></item><item><title>Lesson 3: Global Mutable State — The variable that breaks every test</title><link>/post/go/go-anti-global-state/</link><pubDate>Thu, 05 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-global-state/</guid><description>&lt;p&gt;There was a test suite I worked on where tests passed individually but failed when run together. The failure pattern was non-deterministic — sometimes test A broke test B, sometimes test C broke test A, and it changed with the &lt;code&gt;-count&lt;/code&gt; flag. After two hours of bisecting, we found it: a package-level &lt;code&gt;var config Config&lt;/code&gt; that every test modified by calling &lt;code&gt;loadConfig(&amp;quot;testdata/some-fixture.json&amp;quot;)&lt;/code&gt;. The tests were sharing state without knowing it, and the one that ran last set the config for all the others.&lt;/p&gt;</description></item><item><title>Lesson 3: Idiomatic Naming — Names are your documentation</title><link>/post/go/go-quality-naming/</link><pubDate>Wed, 04 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-naming/</guid><description>&lt;p&gt;When I review Go code, naming problems tell me more about the author&amp;rsquo;s understanding of the codebase than almost anything else. A function called &lt;code&gt;HandleRequest&lt;/code&gt; that parses JSON, queries a database, formats a response, and writes to a log tells me its author didn&amp;rsquo;t know what it does either. A variable called &lt;code&gt;data&lt;/code&gt; in a function that handles three different kinds of data tells me the author stopped thinking halfway through. Names are the first layer of documentation — they&amp;rsquo;re read far more often than comments, and unlike comments, they can&amp;rsquo;t drift out of sync with the code.&lt;/p&gt;</description></item><item><title>Lesson 4: HTTP Handler Testing — httptest is your best friend</title><link>/post/go/go-testing-http-handlers/</link><pubDate>Sun, 01 Sep 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-http-handlers/</guid><description>&lt;p&gt;Every Go HTTP handler is just a function that takes a &lt;code&gt;ResponseWriter&lt;/code&gt; and a &lt;code&gt;*Request&lt;/code&gt;. That&amp;rsquo;s the entire contract. The fact that the standard library ships with a testing package that lets you call that function with a fake writer and a real request — without starting a server, without binding a port — is a gift that too many people overlook. I spent months spinning up actual servers in tests before I discovered &lt;code&gt;net/http/httptest&lt;/code&gt;. I will not let you make the same mistake.&lt;/p&gt;</description></item><item><title>Lesson 3: Circuit Breaking — Stop calling the service that's already down</title><link>/post/go/go-net-circuit-breaker/</link><pubDate>Wed, 28 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-net-circuit-breaker/</guid><description>&lt;p&gt;There&amp;rsquo;s a particular kind of production incident that goes like this: Service A calls Service B. Service B starts responding slowly because its database is under pressure. Service A&amp;rsquo;s goroutines pile up waiting for responses. After a few minutes, Service A is out of memory, and now two services are down instead of one. The postmortem note says &amp;ldquo;cascading failure.&amp;rdquo; The fix, which nobody implemented, is a circuit breaker.&lt;/p&gt;
&lt;p&gt;The circuit breaker pattern comes from electrical engineering. When too much current flows through a circuit, the breaker trips — it opens the circuit and prevents more current from flowing until someone resets it. In software, the &amp;ldquo;current&amp;rdquo; is requests, and &amp;ldquo;tripping&amp;rdquo; means refusing to make calls to a failing downstream instead of piling up timeouts and consuming resources.&lt;/p&gt;</description></item><item><title>Lesson 1: Go Plugins and hashicorp/go-plugin — Extending Go apps without recompiling</title><link>/post/go/go-plugins-basics/</link><pubDate>Mon, 26 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-plugins-basics/</guid><description>&lt;p&gt;The first time I needed a plugin system in Go, I went straight to &lt;code&gt;plugin.Open&lt;/code&gt; in the standard library. Ten minutes later I was reading about RTLD flags and shared library loading on Linux, discovering that plugins had to be compiled with the same Go toolchain version as the host, and learning that once a plugin was loaded it could not be unloaded. My enthusiasm dropped sharply. Then I found &lt;code&gt;hashicorp/go-plugin&lt;/code&gt; and understood that the Go community had largely agreed: real plugin systems in Go should run plugins as separate processes communicating over RPC, not as shared libraries in the same process.&lt;/p&gt;</description></item><item><title>Lesson 3: Method Sets and Addressability — Why your value can't satisfy that interface</title><link>/post/go/go-internals-method-sets/</link><pubDate>Sun, 25 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-internals-method-sets/</guid><description>&lt;p&gt;I spent a solid twenty minutes staring at a compilation error that read &amp;ldquo;does not implement interface (Write method has pointer receiver)&amp;rdquo; and thinking: I &lt;em&gt;can see&lt;/em&gt; the Write method right there. What is Go complaining about? Once I understood method sets and why addressability matters, the rule became obvious — and I have never needed to look it up since.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Go lets you define methods with either a value receiver or a pointer receiver:&lt;/p&gt;</description></item><item><title>Lesson 3: Interface Pollution — More interfaces means more indirection</title><link>/post/go/go-iface-pollution/</link><pubDate>Thu, 22 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-iface-pollution/</guid><description>&lt;p&gt;Interface pollution is not a hypothetical risk. I have walked into codebases where more than half the types in the package are interfaces, where every function parameter is an interface even when the concrete type is never substituted, and where adding a new feature means navigating six files just to trace what a single method call actually does. The code is technically &amp;ldquo;flexible&amp;rdquo; in the sense that every seam is abstract. In practice it is a maze with no map.&lt;/p&gt;</description></item><item><title>Lesson 3: Slice and Map Performance — The data structure tax</title><link>/post/go/go-perf-slice-map/</link><pubDate>Tue, 20 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-perf-slice-map/</guid><description>&lt;p&gt;Slices and maps are so convenient in Go that it&amp;rsquo;s easy to forget they&amp;rsquo;re not free. They have hidden costs — in allocations, in CPU cache misses, in GC scanning time — that only become visible when you push them into a hot path and watch your benchmarks light up. I&amp;rsquo;ve been burned by both, sometimes in embarrassing ways, and building intuition for when those costs matter has saved me more than one production incident.&lt;/p&gt;</description></item><item><title>Lesson 3: internal/ Usage Patterns — Compiler-enforced encapsulation</title><link>/post/go/go-pkg-internal/</link><pubDate>Sun, 18 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-internal/</guid><description>&lt;p&gt;Go does not have access modifiers in the Java or Kotlin sense. There is no &lt;code&gt;protected&lt;/code&gt;, no &lt;code&gt;package-private&lt;/code&gt;, no friend classes. You get exactly two visibility levels: exported (capital letter) and unexported (lowercase letter). That simplicity is a feature. But it creates a gap: how do you share something across packages within your own module without accidentally exposing it to the outside world?&lt;/p&gt;
&lt;p&gt;The answer is &lt;code&gt;internal/&lt;/code&gt;. It is one of the most underused and underappreciated tools in Go&amp;rsquo;s package system, and it is enforced by the compiler itself.&lt;/p&gt;</description></item><item><title>Lesson 2: io Patterns — Reader, Writer, and the composability that makes Go great</title><link>/post/go/go-stdlib-io/</link><pubDate>Fri, 16 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-stdlib-io/</guid><description>&lt;p&gt;The &lt;code&gt;io&lt;/code&gt; package is three hundred lines of interface definitions and a handful of utility functions. It is also the spine of the entire Go standard library. Every package that reads or writes data — &lt;code&gt;os&lt;/code&gt;, &lt;code&gt;net&lt;/code&gt;, &lt;code&gt;compress/gzip&lt;/code&gt;, &lt;code&gt;crypto/tls&lt;/code&gt;, &lt;code&gt;encoding/json&lt;/code&gt;, &lt;code&gt;bufio&lt;/code&gt; — does it through &lt;code&gt;io.Reader&lt;/code&gt; and &lt;code&gt;io.Writer&lt;/code&gt;. Once you understand these two interfaces, the standard library clicks into place as one coherent system.&lt;/p&gt;
&lt;p&gt;I spent my first few months with Go treating &lt;code&gt;io.Reader&lt;/code&gt; as the thing that HTTP response bodies happen to be. It wasn&amp;rsquo;t until I built a streaming CSV processor — piping data from S3 through gzip decompression into a CSV parser, all without loading the file into memory — that I understood what the interfaces were actually designed for.&lt;/p&gt;</description></item><item><title>Lesson 2: Performance Costs — reflect.ValueOf is not free</title><link>/post/go/go-reflect-performance/</link><pubDate>Thu, 15 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-reflect-performance/</guid><description>&lt;p&gt;Every time I see reflection in a hot path, I know there is a performance conversation waiting to happen. Reflection in Go is not arbitrarily slow — it is predictably and measurably slow in specific ways. Understanding those costs, benchmarking them in your context, and knowing the standard mitigation patterns (primarily caching) lets you use reflection where it belongs without making your application noticeably slower.&lt;/p&gt;
&lt;p&gt;The performance story of reflection has two parts: the cost of type inspection (getting a &lt;code&gt;reflect.Type&lt;/code&gt;, reading fields, checking kinds) and the cost of value operations (getting a &lt;code&gt;reflect.Value&lt;/code&gt;, reading or setting field values, calling methods). Type inspection is expensive the first time and cheap if cached. Value operations are expensive every time and there is no way to avoid that cost except to reduce how often you do them.&lt;/p&gt;</description></item><item><title>Lesson 4: Interfaces vs Generics — Behavior or data shape? That's your answer</title><link>/post/go/go-generics-interfaces-vs-generics/</link><pubDate>Wed, 14 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-generics-interfaces-vs-generics/</guid><description>&lt;p&gt;One of the most common mistakes I see in Go codebases post-1.18 is people reaching for generics when interfaces were already the right tool — and vice versa. The two features look superficially similar. Both let you write code that works with multiple types. But they&amp;rsquo;re solving different problems, and using the wrong one makes code harder to read, test, and extend.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the question I ask myself every time: &lt;strong&gt;Is what varies the behavior, or the data shape?&lt;/strong&gt; That single question gets me to the right answer ninety percent of the time.&lt;/p&gt;</description></item><item><title>Lesson 3: TLS Configuration — Your default HTTP server is unencrypted</title><link>/post/go/go-sec-tls/</link><pubDate>Mon, 12 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-sec-tls/</guid><description>&lt;p&gt;When I started writing Go HTTP servers, I thought TLS was someone else&amp;rsquo;s problem. A load balancer in front of my service handled HTTPS termination, so my service only ever spoke plain HTTP on the internal network. That is a common and often defensible architecture. But it means that if anyone gains access to your internal network — a compromised service, a misconfigured cloud security group, a rogue container — all traffic between your services is plaintext. I learned this lesson not from a breach but from a penetration test report that listed it as a finding with a crisp paragraph explaining exactly why it mattered.&lt;/p&gt;</description></item><item><title>Lesson 2: Enhanced HTTP Routing — Method patterns in net/http, no more gorilla/mux</title><link>/post/go/go-modern-http-routing/</link><pubDate>Sun, 11 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-modern-http-routing/</guid><description>&lt;p&gt;For most of my Go career, the standard library&amp;rsquo;s &lt;code&gt;net/http.ServeMux&lt;/code&gt; was something I used for toy services and immediately replaced with &lt;code&gt;gorilla/mux&lt;/code&gt; for anything real. The standard mux could not match HTTP methods. It could not extract path parameters. It matched prefixes in ways that surprised people. The moment a production service needed &lt;code&gt;GET /users/{id}&lt;/code&gt; and &lt;code&gt;POST /users&lt;/code&gt;, you reached for a dependency.&lt;/p&gt;
&lt;p&gt;Go 1.22 changed that. The enhanced &lt;code&gt;ServeMux&lt;/code&gt; now supports method matching, wildcard path parameters, and precedence rules that actually make sense. I rewrote three internal services to drop &lt;code&gt;gorilla/mux&lt;/code&gt; after reading the release notes, and every one of them got shorter and simpler. This lesson covers exactly what changed and when you still might want a third-party router.&lt;/p&gt;</description></item><item><title>Lesson 2: Docker for Go — Multi-stage builds that produce tiny images</title><link>/post/go/go-deploy-docker/</link><pubDate>Sat, 10 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-deploy-docker/</guid><description>&lt;p&gt;The first Dockerfile I wrote for a Go service produced a 1.2 GB image. It was based on &lt;code&gt;golang:1.22&lt;/code&gt;, which includes the full Go toolchain, build cache, test infrastructure, and a complete Debian system. The compiled binary was 18 MB. I was shipping 1.2 GB to run 18 MB.&lt;/p&gt;
&lt;p&gt;The second version, after learning about multi-stage builds, produced a 22 MB image. Same binary. No compiler. No package manager. No shell. Just the binary and the absolute minimum needed to run it. The difference matters not just for storage: smaller images pull faster, have smaller attack surfaces, and fail more obviously when a required file is missing.&lt;/p&gt;</description></item><item><title>Lesson 3: Request Validation — Never trust the caller</title><link>/post/go/go-api-validation/</link><pubDate>Thu, 08 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-api-validation/</guid><description>&lt;p&gt;I once shipped an endpoint that accepted a &lt;code&gt;limit&lt;/code&gt; query parameter for pagination. The valid range was 1–100. I did not validate it. Someone called it with &lt;code&gt;limit=-1&lt;/code&gt; and my database query returned every row in the table. The query took 45 seconds and brought the service to its knees. The fix was a two-line bounds check that I should have written from the start.&lt;/p&gt;
&lt;p&gt;Validation is not optional. It is the contract between your API and the outside world.&lt;/p&gt;</description></item><item><title>Lesson 2: Config and Env Handling — Viper, envconfig, or just os.Getenv?</title><link>/post/go/go-cli-config/</link><pubDate>Mon, 05 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-cli-config/</guid><description>&lt;p&gt;Configuration is one of those problems that looks trivial until it is not. A single environment variable is three lines of code. A configuration file with overridable environment variables, sensible defaults, validation, and reload-on-signal is a project in itself. Knowing when to use each approach — raw &lt;code&gt;os.Getenv&lt;/code&gt;, struct-based env decoding, or a full configuration library like Viper — is more about understanding the tradeoffs than about which library is &amp;ldquo;best.&amp;rdquo;&lt;/p&gt;</description></item><item><title>Lesson 3: Integration Tests — Unit tests lie, integration tests prove</title><link>/post/go/go-testing-integration/</link><pubDate>Thu, 01 Aug 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-integration/</guid><description>&lt;p&gt;Unit tests feel productive. You write a function, you write a test, everything goes green, and you merge. But unit tests exist in a bubble — a bubble where every dependency returns exactly what you told it to return. The real world doesn&amp;rsquo;t do that. Your database serializes a &lt;code&gt;time.Time&lt;/code&gt; differently than you expect. Your HTTP client follows a redirect your mock never mentioned. Your message queue drops a message under backpressure that your fake queue cheerfully delivered. I&amp;rsquo;ve had unit test suites where every test passed and the feature didn&amp;rsquo;t work in staging. Integration tests are what close that gap.&lt;/p&gt;</description></item><item><title>Lesson 1: Building MCP Servers in Go — Give AI agents tools with the Model Context Protocol</title><link>/post/go/go-ai-mcp-servers/</link><pubDate>Tue, 30 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-ai-mcp-servers/</guid><description>&lt;p&gt;When Claude or another AI assistant needs to look up a database record, call an internal API, or read a file from your filesystem, it can&amp;rsquo;t do that on its own — it needs tools. The Model Context Protocol (MCP) is Anthropic&amp;rsquo;s open standard for giving AI agents exactly those tools. An MCP server is a small program you write that exposes tools via a JSON-RPC protocol; the AI client calls your server to invoke them. I find this genuinely exciting as a Go developer: Go&amp;rsquo;s concurrency model and fast startup time make it a natural fit for MCP servers.&lt;/p&gt;</description></item><item><title>Lesson 2: Metrics That Matter — Count, measure, alert</title><link>/post/go/go-obs-metrics/</link><pubDate>Sun, 28 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-obs-metrics/</guid><description>&lt;p&gt;The first metrics dashboard I built for a Go service had forty-two graphs. CPU, memory, goroutine count, heap allocations, GC pause duration, request rate, error rate, and about thirty-five other things that felt important when I added them. Six months later I was on call at 3 AM and the service was degraded. I opened that dashboard, looked at forty-two graphs, and had no idea where to start.&lt;/p&gt;
&lt;p&gt;Metrics are only useful when you know what to alert on, and you can only alert on things you understand. More metrics is not the same as better observability. The question is not &amp;ldquo;what can I measure?&amp;rdquo; but &amp;ldquo;what breaks, how do I know it&amp;rsquo;s broken, and how quickly can I narrow down why?&amp;rdquo;&lt;/p&gt;</description></item><item><title>Lesson 2: Giant God Structs — If your struct has 30 fields, it has 30 problems</title><link>/post/go/go-anti-god-structs/</link><pubDate>Thu, 25 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-god-structs/</guid><description>&lt;p&gt;I inherited a Go service where the primary domain object was a struct with 47 fields. Some were database columns, some were computed from other fields, some were HTTP response projections, some were used only during a specific workflow stage, and a handful were there because someone once needed a flag and adding a field to the God struct was the path of least resistance. The struct was everywhere — passed between functions, serialized to JSON, written to the database, and used as a GraphQL response type. Every change to it rippled through the entire codebase.&lt;/p&gt;</description></item><item><title>Lesson 2: Spotting Over-Abstraction — When your abstraction is the problem</title><link>/post/go/go-quality-over-abstraction/</link><pubDate>Wed, 24 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-over-abstraction/</guid><description>&lt;p&gt;There&amp;rsquo;s a version of Go code I see in almost every codebase that&amp;rsquo;s been touched by developers who came from Java or C# — it&amp;rsquo;s covered in interfaces, repositories, factories, and service layers that all wrap exactly one concrete implementation. No tests mock these interfaces. No second implementation exists. The abstraction is doing nothing except adding indirection. I&amp;rsquo;ve written code like this myself, and the tell is always the same: when something breaks, I have to jump through five files to understand what happens when I call &lt;code&gt;CreateUser&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Lesson 1: CGo Basics — When you need C and how to call it safely</title><link>/post/go/go-cgo-basics/</link><pubDate>Mon, 22 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-cgo-basics/</guid><description>&lt;p&gt;There is a saying in the Go community: &amp;ldquo;cgo is not Go.&amp;rdquo; It is not an insult. It is a warning. The moment you add &lt;code&gt;import &amp;quot;C&amp;quot;&lt;/code&gt; to a file, you are no longer writing a pure Go program — you are writing a Go program that manages a C boundary, and all the things that make Go comfortable (fast builds, easy cross-compilation, the race detector, straightforward stack traces) become harder. You are doing it on purpose, because the alternative is worse.&lt;/p&gt;</description></item><item><title>Lesson 3: Good Patterns — Generic code should be boring</title><link>/post/go/go-generics-good-patterns/</link><pubDate>Mon, 22 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-generics-good-patterns/</guid><description>&lt;p&gt;The best generic code I&amp;rsquo;ve ever written looks like the worst. No clever type wizardry, no five-level constraint hierarchies — just a function that clearly does one thing, works for any type that makes sense, and disappears into the background of a codebase. When someone reads it two years later and immediately understands it, that&amp;rsquo;s a win.&lt;/p&gt;
&lt;p&gt;This lesson is about the patterns where generics genuinely shine. These are the ones that survived code review, that made the team&amp;rsquo;s lives measurably better, and that I&amp;rsquo;d reach for again without hesitation.&lt;/p&gt;</description></item><item><title>Lesson 2: Retries with Exponential Backoff — Retry right or retry forever</title><link>/post/go/go-net-retries/</link><pubDate>Sat, 20 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-net-retries/</guid><description>&lt;p&gt;I once worked on a service that retried failed HTTP requests in a tight loop with no backoff and no jitter. When our upstream had a brief outage, every instance of our service fired retries simultaneously, on the same cadence, forever. The upstream came back online, got immediately hammered by a synchronized retry storm from fifty instances, went down again, and the cycle repeated for forty minutes. The &amp;ldquo;retry&amp;rdquo; logic had turned a five-minute outage into a cascading failure.&lt;/p&gt;</description></item><item><title>Lesson 2: Nil Interface Gotchas — nil is not nil when it has a type</title><link>/post/go/go-internals-nil-interface/</link><pubDate>Thu, 18 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-internals-nil-interface/</guid><description>&lt;p&gt;A few months into my first production Go service, I had a bug that cost me two hours. A function returned an &lt;code&gt;error&lt;/code&gt;, I checked &lt;code&gt;if err != nil&lt;/code&gt; and it passed — but then further down the stack the program panicked trying to call &lt;code&gt;.Error()&lt;/code&gt; on a nil pointer. The function had returned a nil &lt;code&gt;*MyError&lt;/code&gt;, not a nil &lt;code&gt;error&lt;/code&gt;. These are not the same thing, and until you internalize why, you will hit this bug.&lt;/p&gt;</description></item><item><title>Lesson 2: Stack vs Heap Intuition — The allocation you didn't know you made</title><link>/post/go/go-perf-stack-heap/</link><pubDate>Tue, 16 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-perf-stack-heap/</guid><description>&lt;p&gt;There&amp;rsquo;s a class of performance bugs in Go that doesn&amp;rsquo;t show up in code review, doesn&amp;rsquo;t trigger the race detector, and doesn&amp;rsquo;t cause test failures. It just makes your service slowly worse under load. The source is almost always the same: heap allocations happening in places you didn&amp;rsquo;t intend, turning what should be fast stack operations into GC-visible objects that pile up until the collector has to stop and clean them up. Building an intuition for when Go allocates on the heap versus the stack was one of the single highest-leverage things I did to improve the services I work on.&lt;/p&gt;</description></item><item><title>Lesson 2: Consumer-Side Interfaces — Define where you use, not where you build</title><link>/post/go/go-iface-consumer-side/</link><pubDate>Mon, 15 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-iface-consumer-side/</guid><description>&lt;p&gt;There is a convention I spent a long time following without questioning it: put the interface in the same package as the type that implements it. If I had a &lt;code&gt;payment&lt;/code&gt; package with a &lt;code&gt;Stripe&lt;/code&gt; struct, I would also define &lt;code&gt;PaymentProcessor&lt;/code&gt; right there. Callers would import &lt;code&gt;payment.PaymentProcessor&lt;/code&gt;. It felt natural — the interface lives next to the thing it describes.&lt;/p&gt;
&lt;p&gt;Go&amp;rsquo;s designers had a different idea in mind, and it took me several refactoring sessions on a real codebase before I understood why it matters: interfaces should be defined by the package that uses them, not the package that implements them. The Go standard library does this consistently and for good reason.&lt;/p&gt;</description></item><item><title>Lesson 2: Avoiding Cyclic Dependencies — If packages import each other, your design is wrong</title><link>/post/go/go-pkg-cyclic-deps/</link><pubDate>Fri, 12 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-cyclic-deps/</guid><description>&lt;p&gt;The Go compiler refuses to build a program with a cyclic import. It is not a warning. It is not a lint violation. It is a hard failure. I used to find this annoying when I was newer to the language. Now I consider it one of Go&amp;rsquo;s greatest gifts. The compiler is telling you something important: if package A needs package B and package B needs package A, you have not yet understood what the relationship between these two concepts really is.&lt;/p&gt;</description></item><item><title>Lesson 2: Fuzzing in Go — Let the machine find your edge cases</title><link>/post/go/go-testing-fuzzing/</link><pubDate>Wed, 10 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-fuzzing/</guid><description>&lt;p&gt;There&amp;rsquo;s a class of bugs that you will never find by thinking about edge cases. You&amp;rsquo;ll think about empty strings, about zero values, about negative numbers. But will you think about the string that&amp;rsquo;s exactly 65,536 bytes? Or the UTF-8 sequence that&amp;rsquo;s technically valid but trips up a specific parser codepath? Or the floating point value that serializes and then fails to deserialize because of a precision edge in your JSON handling? You won&amp;rsquo;t. But a fuzzer will find it in under a minute.&lt;/p&gt;</description></item><item><title>Lesson 2: Secret Handling — Env vars are not a vault</title><link>/post/go/go-sec-secrets/</link><pubDate>Mon, 08 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-sec-secrets/</guid><description>&lt;p&gt;I once found database credentials committed directly in a Go source file — not in a toy project, in a staging environment of a real product. The developer who wrote it knew it was wrong, but they were &amp;ldquo;just testing&amp;rdquo; and forgot to revert it. That file lived in the repository for eight months before anyone noticed. By then, the credentials had been rotated, but the history was permanent.&lt;/p&gt;
&lt;p&gt;Secret handling in Go is not primarily a coding problem. It is a discipline problem. The code patterns are simple. The failures happen when you treat secrets as a detail to clean up later, or when you assume that environment variables are inherently safe because they are not in source code.&lt;/p&gt;</description></item><item><title>Lesson 1: Service Boundaries — Split by business capability, not by technical layer</title><link>/post/go/go-micro-boundaries/</link><pubDate>Sat, 06 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-micro-boundaries/</guid><description>&lt;p&gt;The most expensive architectural mistake I&amp;rsquo;ve seen teams make with microservices is splitting by technical layer instead of by business capability. I&amp;rsquo;ve seen systems with a &amp;ldquo;UserDataService,&amp;rdquo; a &amp;ldquo;UserLogicService,&amp;rdquo; and a &amp;ldquo;UserNotificationService&amp;rdquo; — three services that all have to be deployed together for any user-related feature to work, that share a database, and that call each other synchronously for every request. They&amp;rsquo;re not microservices; they&amp;rsquo;re a distributed monolith with extra network latency.&lt;/p&gt;</description></item><item><title>Lesson 2: Middleware Patterns — Wrap the handler, not the logic</title><link>/post/go/go-api-middleware/</link><pubDate>Fri, 05 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-api-middleware/</guid><description>&lt;p&gt;The first time I wrote authentication logic, I put it directly inside each handler. Copy-paste, then copy-paste again. By handler number five I had four slightly different versions of the same JWT check scattered across the codebase. When the security team asked me to add a new validation step, I had to find and update every single one. That was the day I understood middleware.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Business logic in a handler should answer one question: &amp;ldquo;What does this endpoint do?&amp;rdquo; But handlers inevitably accumulate cross-cutting concerns — logging, authentication, rate limiting, request ID injection, panic recovery. When these live inside the handler body, every handler becomes a tangle of concerns that are hard to test, hard to change, and impossible to apply consistently.&lt;/p&gt;</description></item><item><title>Lesson 1: net/http Deep Dive — The most important package you use wrong</title><link>/post/go/go-stdlib-net-http/</link><pubDate>Tue, 02 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-stdlib-net-http/</guid><description>&lt;p&gt;Every Go web service I&amp;rsquo;ve reviewed starts with &lt;code&gt;net/http&lt;/code&gt;. Most of them use it correctly for the obvious parts and incorrectly for the subtle ones. The package API is deceptively simple — &lt;code&gt;http.HandleFunc&lt;/code&gt;, &lt;code&gt;http.ListenAndServe&lt;/code&gt;, and you&amp;rsquo;re serving HTTP. But the design decisions underneath — how request routing works, what a &lt;code&gt;Handler&lt;/code&gt; actually is, how the server manages connections, what the default timeouts are — contain enough traps to keep a senior engineer busy for a week.&lt;/p&gt;</description></item><item><title>Lesson 2: Type Parameters and Constraints — Teaching the compiler what you mean</title><link>/post/go/go-generics-type-parameters/</link><pubDate>Mon, 01 Jul 2024 00:00:00 +0000</pubDate><guid>/post/go/go-generics-type-parameters/</guid><description>&lt;p&gt;The hardest part of learning Go generics isn&amp;rsquo;t the concept — it&amp;rsquo;s the syntax. The first time I read a function signature like &lt;code&gt;func Keys[K comparable, V any](m map[K]V) []K&lt;/code&gt;, I did a double-take. It looks like someone snuck type annotations from another language into Go. But once it clicks, it&amp;rsquo;s actually pretty elegant — every piece is there for a reason.&lt;/p&gt;
&lt;p&gt;This lesson is about understanding type parameters and constraints deeply enough that you can write them yourself without copying from examples. That&amp;rsquo;s the threshold that separates &amp;ldquo;I&amp;rsquo;ve used generics&amp;rdquo; from &amp;ldquo;I understand generics.&amp;rdquo;&lt;/p&gt;</description></item><item><title>Lesson 1: Static Binaries — One file, zero dependencies, deploy anywhere</title><link>/post/go/go-deploy-static-binaries/</link><pubDate>Sun, 30 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-deploy-static-binaries/</guid><description>&lt;p&gt;The first time I deployed a Go binary to a production server, I was braced for the usual ceremony: SSH in, install the right runtime version, copy over config files, fiddle with symlinks. Instead, I ran &lt;code&gt;scp&lt;/code&gt; and then the binary, and it just worked. No installation. No dependency hell. No &amp;ldquo;but it works on my machine.&amp;rdquo; The binary contained everything it needed.&lt;/p&gt;
&lt;p&gt;This is Go&amp;rsquo;s single greatest operational advantage: the compiler produces a self-contained executable. Understanding exactly how this works — and how it can break — makes the difference between a deploy that takes five minutes and one that takes an afternoon.&lt;/p&gt;</description></item><item><title>Lesson 1: When Reflection Is Justified — The last resort that sometimes is the only resort</title><link>/post/go/go-reflect-when-justified/</link><pubDate>Fri, 28 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-reflect-when-justified/</guid><description>&lt;p&gt;Go&amp;rsquo;s &lt;code&gt;reflect&lt;/code&gt; package carries a reputation: &amp;ldquo;don&amp;rsquo;t use it unless you have to.&amp;rdquo; That advice is correct but incomplete. Understanding &lt;em&gt;when&lt;/em&gt; you actually have to use it — and when you are just reaching for it out of habit from dynamic languages — is the difference between reflection that pulls its weight and reflection that creates unmaintainable, slow, panic-prone code that confuses every future reader of the codebase.&lt;/p&gt;
&lt;p&gt;Reflection lets Go inspect and manipulate values, types, and struct fields at runtime without static type information. It is the mechanism behind &lt;code&gt;encoding/json&lt;/code&gt;, &lt;code&gt;database/sql&lt;/code&gt;&amp;rsquo;s row scanning, struct validators, ORM frameworks, and dependency injection containers. It is also the mechanism behind a lot of code that should have just used a map or an interface.&lt;/p&gt;</description></item><item><title>Lesson 1: Structured Logging with slog — Printf is not observability</title><link>/post/go/go-obs-slog/</link><pubDate>Tue, 25 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-obs-slog/</guid><description>&lt;p&gt;I spent two years writing Go services that logged like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;log&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Printf&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;processing order %s for user %d, amount %.2f&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;orderID&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;userID&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;amount&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It worked fine — until the day I needed to find every failed payment over $500 from a specific user across three weeks of logs in a production incident at 2 AM. I was grepping through gigabytes of text, trying to parse freeform strings with jq, getting nowhere. The logs existed. The information was technically there. But I couldn&amp;rsquo;t &lt;em&gt;query&lt;/em&gt; it in any meaningful way.&lt;/p&gt;</description></item><item><title>Lesson 1: Building CLIs with Cobra — From main.go to production CLI</title><link>/post/go/go-cli-cobra/</link><pubDate>Sat, 22 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-cli-cobra/</guid><description>&lt;p&gt;Every Go project that grows into something useful eventually needs a command-line interface. Maybe it starts as a quick &lt;code&gt;main.go&lt;/code&gt; with &lt;code&gt;os.Args[1]&lt;/code&gt; checks and a switch statement. That works until you need subcommands, flags, help text, shell completion, and version information — and suddenly you are maintaining a hand-rolled argument parser that nobody wants to touch. Cobra is the standard library-grade solution to this problem, and learning to structure a Cobra application properly saves you from rewriting it twice.&lt;/p&gt;</description></item><item><title>Lesson 1: Escape Analysis — Where your data lives matters more than how you write it</title><link>/post/go/go-perf-escape-analysis/</link><pubDate>Thu, 20 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-perf-escape-analysis/</guid><description>&lt;p&gt;I spent the first two years of writing Go completely unaware that the compiler was making decisions about my code that I never asked for — and that those decisions were quietly shaping the performance profile of everything I shipped. Escape analysis is the mechanism behind all of it. Once I understood it, I started reading code differently. Not just &amp;ldquo;does this work?&amp;rdquo; but &amp;ldquo;where does this data live, and did I give the compiler a chance to put it somewhere fast?&amp;rdquo;&lt;/p&gt;</description></item><item><title>Lesson 1: Interface Everywhere Syndrome — Not every dependency needs an interface</title><link>/post/go/go-anti-interface-everywhere/</link><pubDate>Tue, 18 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-anti-interface-everywhere/</guid><description>&lt;p&gt;When I came to Go from a Java background, I brought some habits with me that made my code worse, not better. The most persistent one was defining an interface for every dependency, regardless of whether there was any realistic chance of substituting an alternative implementation. I wrote &lt;code&gt;UserRepositoryInterface&lt;/code&gt;, &lt;code&gt;EmailSenderInterface&lt;/code&gt;, &lt;code&gt;LoggerInterface&lt;/code&gt; — an entire shadow type system that mirrored every concrete type in the codebase. Every file had a corresponding interface file. The codebase was twice as large as it needed to be and no easier to test.&lt;/p&gt;</description></item><item><title>Lesson 1: Refactoring Go Code Safely — Change structure without changing behavior</title><link>/post/go/go-quality-refactoring/</link><pubDate>Sun, 16 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-quality-refactoring/</guid><description>&lt;p&gt;Refactoring is the one activity that makes codebases better without adding features — and it&amp;rsquo;s also the activity most likely to introduce bugs if done carelessly. I learned this the hard way on a payments service where I renamed a function, ran the tests, saw green, deployed, and watched a webhook handler silently stop processing because it had been calling the old function name through a string-based registry I hadn&amp;rsquo;t touched. The tests were green because the old function still existed — I just hadn&amp;rsquo;t deleted it yet. The refactor was correct; the process was not.&lt;/p&gt;</description></item><item><title>Lesson 1: Subtests and t.Run — Name your test cases or debug blind</title><link>/post/go/go-testing-subtests/</link><pubDate>Sat, 15 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-testing-subtests/</guid><description>&lt;p&gt;I used to write tests the way I wrote the first functions I ever wrote in Go — flat, repetitive, and with names so generic that when they failed, I had no idea which case broke. &amp;ldquo;TestParseDate failed&amp;rdquo; is not information. It&amp;rsquo;s a dare. Go figure out which of the twelve implicit cases in the body is responsible. I wasted hours doing exactly that before I committed to subtests.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;t.Run&lt;/code&gt; is one of those language features that looks like a small convenience and turns out to be load-bearing for any serious test suite. Once you start using it, you wonder how you ever shipped anything without it.&lt;/p&gt;</description></item><item><title>Lesson 1: HTTP Client Internals — Transport, connection reuse, and the pool you forgot to configure</title><link>/post/go/go-net-http-client/</link><pubDate>Fri, 14 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-net-http-client/</guid><description>&lt;p&gt;The first production Go service I wrote hammered our database proxy with thousands of new TCP connections every minute. The proxy started refusing connections. The on-call engineer thought it was the database. It wasn&amp;rsquo;t. It was me — specifically, my decision to create a new &lt;code&gt;http.Client&lt;/code&gt; on every request and then never read the response body to completion. I spent three hours debugging what turned out to be two lines of misunderstood standard library behavior.&lt;/p&gt;</description></item><item><title>Lesson 1: Designing HTTP APIs in Go — net/http is enough</title><link>/post/go/go-api-http-design/</link><pubDate>Wed, 12 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-api-http-design/</guid><description>&lt;p&gt;I spent the first few months of writing Go services reaching straight for a framework. Chi, Gorilla, Echo — I cycled through them trying to find the &amp;ldquo;right&amp;rdquo; one. Then a colleague reviewed my code and asked a simple question: &amp;ldquo;What does this framework give you that &lt;code&gt;net/http&lt;/code&gt; doesn&amp;rsquo;t?&amp;rdquo; I couldn&amp;rsquo;t answer. That conversation changed how I think about HTTP in Go.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Most developers coming from Node.js or Python assume they need a framework to build a real HTTP API. Express, FastAPI, Django — these tools abstract away so much of the protocol that going back to the raw standard library feels like stepping down. But Go&amp;rsquo;s &lt;code&gt;net/http&lt;/code&gt; package is not an afterthought. It was designed with production use in mind, and the gap between it and popular frameworks is much smaller than you&amp;rsquo;d expect.&lt;/p&gt;</description></item><item><title>Lesson 1: Interface Representation — Two words that carry your abstractions</title><link>/post/go/go-internals-interfaces/</link><pubDate>Mon, 10 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-internals-interfaces/</guid><description>&lt;p&gt;I remember the first time I hit a bug where &lt;code&gt;err != nil&lt;/code&gt; returned &lt;code&gt;true&lt;/code&gt; even though the function clearly returned &lt;code&gt;nil&lt;/code&gt;. I spent forty minutes staring at perfectly reasonable-looking code before I realized I had no idea what an interface actually &lt;em&gt;is&lt;/em&gt; at the memory level. Once I understood the two-word representation, that class of bug never confused me again.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Go interfaces are the backbone of polymorphism in the language. You use them constantly — &lt;code&gt;io.Reader&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;fmt.Stringer&lt;/code&gt;. But most developers treat them as magic. You assign a concrete value, you call methods, and somehow Go figures out which implementation to dispatch to at runtime.&lt;/p&gt;</description></item><item><title>Lesson 1: When NOT to Use Interfaces — Abstractions cost clarity</title><link>/post/go/go-iface-when-not/</link><pubDate>Sat, 08 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-iface-when-not/</guid><description>&lt;p&gt;The first time I saw Go interfaces click in a codebase, I made the same mistake every developer makes: I started reaching for them everywhere. If I had a struct, I made an interface for it. If I had two types that shared even one method name, I defined an interface over both. It felt disciplined. It felt like proper software engineering. It was making my code worse.&lt;/p&gt;
&lt;p&gt;The uncomfortable truth about Go interfaces is that they are most powerful when you use them least. Unlike Java, where every class implementing an interface is an explicit declaration, Go interfaces are satisfied implicitly. That means there is almost zero syntax cost to adding one. And that near-zero cost hides the real cost: cognitive overhead, indirection, and the loss of explicit signal about what a piece of code actually does.&lt;/p&gt;</description></item><item><title>Lesson 1: What Generics Solve in Go — Remove duplication without hiding intent</title><link>/post/go/go-generics-what-they-solve/</link><pubDate>Fri, 07 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-generics-what-they-solve/</guid><description>&lt;p&gt;I spent two years writing Go before generics shipped in 1.18, and I&amp;rsquo;ll be honest — I didn&amp;rsquo;t miss them at first. Go&amp;rsquo;s simplicity felt like a feature. Then I found myself maintaining a codebase with six nearly-identical sort functions, a &lt;code&gt;MinInt&lt;/code&gt;, &lt;code&gt;MinFloat64&lt;/code&gt;, &lt;code&gt;MinInt64&lt;/code&gt;, and a comment that said &amp;ldquo;do not touch, generated.&amp;rdquo; That&amp;rsquo;s when I started caring.&lt;/p&gt;
&lt;p&gt;Generics in Go aren&amp;rsquo;t a revolution. They&amp;rsquo;re a targeted fix for a specific, real pain point: you had to either use &lt;code&gt;interface{}&lt;/code&gt; and lose type safety, or repeat yourself across every concrete type you cared about. Neither option aged well.&lt;/p&gt;</description></item><item><title>Lesson 1: Input Validation — Trust nothing from the wire</title><link>/post/go/go-sec-input-validation/</link><pubDate>Wed, 05 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-sec-input-validation/</guid><description>&lt;p&gt;The first production security incident I dealt with involved a simple integer field in a JSON body. The client sent a negative number where we expected a positive quantity. We hadn&amp;rsquo;t validated it. The result was a negative charge on a purchase — we were effectively paying customers money. That taught me more about input validation than any security course.&lt;/p&gt;
&lt;p&gt;Every byte that arrives over the wire is adversarial by default. Not because every user is malicious, but because your assumptions about the data are not enforced by anything except your own code. HTTP gives you a byte stream. JSON decoding gives you Go values. Neither of those steps checks whether the values make sense for your business logic. That job is yours.&lt;/p&gt;</description></item><item><title>Lesson 1: Range Over Integers and Iterators — for i := range 10 and the iterator protocol</title><link>/post/go/go-modern-range-iterators/</link><pubDate>Tue, 04 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-modern-range-iterators/</guid><description>&lt;p&gt;I have written &lt;code&gt;for i := 0; i &amp;lt; n; i++&lt;/code&gt; so many times that my fingers type it on autopilot. The three-clause for loop is fine — it works, everyone understands it, and Go has always had it. But when Go 1.22 shipped &lt;code&gt;for i := range 10&lt;/code&gt;, I stopped and stared at it for a moment. Not because it was complicated. Because it was so obviously the right thing that I wondered why it had taken this long.&lt;/p&gt;</description></item><item><title>Lesson 1: Package Boundaries — One package, one responsibility, zero excuses</title><link>/post/go/go-pkg-boundaries/</link><pubDate>Mon, 03 Jun 2024 00:00:00 +0000</pubDate><guid>/post/go/go-pkg-boundaries/</guid><description>&lt;p&gt;I have refactored a lot of Go codebases — my own included. And the single most common structural mistake I see is the &lt;code&gt;utils&lt;/code&gt; package. Or &lt;code&gt;helpers&lt;/code&gt;. Or &lt;code&gt;common&lt;/code&gt;. Or sometimes the worst offender of all: a package named after the entire application domain with fifty unrelated files sitting inside it. When I first started writing Go seriously, I made all of these mistakes. This lesson is about why package boundaries matter and how to get them right.&lt;/p&gt;</description></item><item><title>Lesson 25: What's Next — Your roadmap from beginner to production Go</title><link>/post/go/go-scratch-whats-next/</link><pubDate>Sat, 11 May 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-whats-next/</guid><description>&lt;p&gt;You made it. Twenty-five lessons ago you didn&amp;rsquo;t know what &lt;code&gt;package main&lt;/code&gt; meant. Now you understand variables, types, functions, error handling, interfaces, goroutines, testing, JSON, file I/O, the type system, and the entire toolchain. That is not beginner knowledge anymore.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s a gap between knowing the language and writing Go the way experienced Go developers write it. This lesson is about that gap — what it is, how to close it, and what to build while you do.&lt;/p&gt;</description></item><item><title>Lesson 24: The Go Toolchain — build, test, vet, fmt — your daily tools</title><link>/post/go/go-scratch-toolchain/</link><pubDate>Tue, 07 May 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-toolchain/</guid><description>&lt;p&gt;One of the things that drew me to Go was that the toolchain ships with the language. You don&amp;rsquo;t need to choose a build tool, a formatter, or a test framework — they&amp;rsquo;re all there from day one, and they&amp;rsquo;re all invoked the same way: &lt;code&gt;go &amp;lt;command&amp;gt;&lt;/code&gt;. This lesson is a tour of the tools you&amp;rsquo;ll use every single day.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-basics"&gt;The Basics&lt;/h2&gt;
&lt;h3 id="go-run-and-go-build"&gt;go run and go build&lt;/h3&gt;
&lt;p&gt;You&amp;rsquo;ve used &lt;code&gt;go run&lt;/code&gt; throughout this course. It compiles your code and immediately executes it — perfect for development. Under the hood it creates a temporary binary and runs it, then throws the binary away.&lt;/p&gt;</description></item><item><title>Lesson 23: The Type System — Types, aliases, conversions, and embedding</title><link>/post/go/go-scratch-type-system/</link><pubDate>Thu, 02 May 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-type-system/</guid><description>&lt;p&gt;When I first came to Go from Python, the type system felt like a lot of ceremony. Why can&amp;rsquo;t I just use an &lt;code&gt;int&lt;/code&gt; where a &lt;code&gt;float64&lt;/code&gt; is expected? Why do I need to define a whole new type just to give an integer more meaning? The answers to those questions turned out to be one of the things I now appreciate most about Go. Types aren&amp;rsquo;t bureaucracy — they&amp;rsquo;re a way of making your code explain itself.&lt;/p&gt;</description></item><item><title>Lesson 22: File I/O — Reading and writing files the Go way</title><link>/post/go/go-scratch-files/</link><pubDate>Fri, 26 Apr 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-files/</guid><description>&lt;p&gt;At some point every program needs to persist something — a config file, a log, a data export. File I/O is one of those topics that sounds tedious but is genuinely satisfying once it clicks. Go gives you a few different layers to work with, from the low-level &lt;code&gt;os&lt;/code&gt; package to convenient one-liners. I&amp;rsquo;ll show you all of them so you can pick the right tool for the job.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-basics"&gt;The Basics&lt;/h2&gt;
&lt;h3 id="reading-a-file-the-simple-way"&gt;Reading a file the simple way&lt;/h3&gt;
&lt;p&gt;For most cases where the file is small enough to fit in memory, &lt;code&gt;os.ReadFile&lt;/code&gt; is all you need:&lt;/p&gt;</description></item><item><title>Lesson 21: JSON and HTTP — Reading and writing the web's language</title><link>/post/go/go-scratch-json/</link><pubDate>Sun, 21 Apr 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-json/</guid><description>&lt;p&gt;Almost every program I write these days talks to something over HTTP — a third-party API, a database service, a webhook. And almost everything speaks JSON. So learning how Go handles JSON and HTTP isn&amp;rsquo;t an advanced topic; it&amp;rsquo;s one of the most practical skills you can pick up early.&lt;/p&gt;
&lt;p&gt;In this lesson I&amp;rsquo;ll walk you through converting Go structs to JSON and back, decoding JSON from an API response, and making real HTTP requests — all using only Go&amp;rsquo;s standard library. No third-party packages needed.&lt;/p&gt;</description></item><item><title>Lesson 20: Testing in Go — Every Go file gets a test file</title><link>/post/go/go-scratch-testing/</link><pubDate>Wed, 17 Apr 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-testing/</guid><description>&lt;p&gt;I used to think testing was something you did after you finished writing code — a checkbox you ticked before committing. Go changed that attitude for me, not by lecturing me about best practices, but by making testing so straightforward that there was no excuse not to do it. The tooling is built in, the conventions are clear, and writing a test in Go takes about as long as writing the function itself. After a few weeks with Go, I started writing tests alongside my code, then slightly before it. The feedback loop became addictive.&lt;/p&gt;</description></item><item><title>Lesson 19: Context — Passing deadlines and cancellation through your code</title><link>/post/go/go-scratch-context/</link><pubDate>Fri, 12 Apr 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-context/</guid><description>&lt;p&gt;There&amp;rsquo;s a moment in every Go developer&amp;rsquo;s journey when they realize that starting an operation isn&amp;rsquo;t the hard part — stopping it cleanly is. What happens when a user cancels a request? What happens when a database query takes 30 seconds instead of 300 milliseconds? Without a way to communicate &amp;ldquo;stop what you&amp;rsquo;re doing,&amp;rdquo; those goroutines keep running, consuming resources for something that no longer matters.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the problem &lt;code&gt;context&lt;/code&gt; solves. It gives you a standard way to carry a cancellation signal, a deadline, and a small amount of request-scoped data through your entire call stack. Once you understand it, you&amp;rsquo;ll see why the Go standard library passes it as the first parameter to almost every function that does I/O.&lt;/p&gt;</description></item><item><title>Lesson 18: The sync Package — Coordination tools for concurrent code</title><link>/post/go/go-scratch-sync/</link><pubDate>Sat, 06 Apr 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-sync/</guid><description>&lt;p&gt;Goroutines make concurrency easy to start, but easy to start isn&amp;rsquo;t the same as easy to get right. The first time I ran multiple goroutines that touched shared data, I got a data race — a situation where two goroutines read and write the same variable at the same time, producing unpredictable results. The Go race detector caught it immediately, but I still had to understand &lt;em&gt;how to fix it&lt;/em&gt;.&lt;/p&gt;</description></item><item><title>Lesson 17: Go Modules — Managing dependencies without pain</title><link>/post/go/go-scratch-modules/</link><pubDate>Tue, 02 Apr 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-modules/</guid><description>&lt;p&gt;Before Go modules existed, managing dependencies was genuinely painful. People used all sorts of third-party tools with their own conventions, and getting a new contributor up and running on a project could eat half a day. I started learning Go after modules became the standard, and I took for granted how smooth the experience was — until I read the old blog posts describing what came before. It made me appreciate &lt;code&gt;go mod init&lt;/code&gt; in a way that&amp;rsquo;s hard to describe.&lt;/p&gt;</description></item><item><title>Lesson 16: Packages and Imports — Organizing code like a professional</title><link>/post/go/go-scratch-packages/</link><pubDate>Wed, 27 Mar 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-packages/</guid><description>&lt;p&gt;Every Go program I&amp;rsquo;ve written has taught me the same lesson over and over: good code isn&amp;rsquo;t just about what you write inside functions — it&amp;rsquo;s about how you organize those functions in the first place. When I started with Go, I crammed everything into one file. It worked, until it didn&amp;rsquo;t. By the time I had a few hundred lines, I couldn&amp;rsquo;t find anything. That&amp;rsquo;s when packages stopped being an abstract concept and became something I genuinely needed.&lt;/p&gt;</description></item><item><title>Lesson 15: Select — Waiting on multiple channels at once</title><link>/post/go/go-scratch-select/</link><pubDate>Fri, 22 Mar 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-select/</guid><description>&lt;p&gt;By now you know how to create goroutines and how to communicate between them using channels. But what happens when a goroutine needs to listen to two channels at the same time? Maybe it&amp;rsquo;s waiting for work to arrive, but it also needs to stop if a timeout fires. With a single channel you&amp;rsquo;d be stuck — receiving from one blocks the other.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt; solves this. It&amp;rsquo;s like a &lt;code&gt;switch&lt;/code&gt; statement for channels. It waits until one of several channel operations is ready, then executes that case. If multiple are ready at the same time, it picks one at random.&lt;/p&gt;</description></item><item><title>Lesson 14: Channels — How goroutines talk to each other</title><link>/post/go/go-scratch-channels/</link><pubDate>Mon, 18 Mar 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-channels/</guid><description>&lt;p&gt;In the last lesson, I showed you how to run code concurrently with goroutines. But goroutines that can&amp;rsquo;t talk to each other aren&amp;rsquo;t very useful. What if one goroutine produces a result that another needs? What if you want to signal that work is done without using a &lt;code&gt;WaitGroup&lt;/code&gt;? That&amp;rsquo;s where channels come in.&lt;/p&gt;
&lt;p&gt;Go&amp;rsquo;s guiding philosophy on concurrency is: &amp;ldquo;don&amp;rsquo;t communicate by sharing memory; share memory by communicating.&amp;rdquo; Channels are the mechanism for that. Instead of multiple goroutines reading and writing the same variable, they pass values through a channel — and the channel ensures only one goroutine handles the value at a time.&lt;/p&gt;</description></item><item><title>Lesson 13: Goroutines — Lightweight threads that cost almost nothing</title><link>/post/go/go-scratch-goroutines/</link><pubDate>Tue, 12 Mar 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-goroutines/</guid><description>&lt;p&gt;One of the first things people told me about Go was &amp;ldquo;it makes concurrency easy.&amp;rdquo; I was sceptical — concurrency is never easy. But the more I used goroutines, the more I understood what they meant. It&amp;rsquo;s not that concurrency becomes simple. It&amp;rsquo;s that the cost of starting a concurrent task drops so low that you stop avoiding it.&lt;/p&gt;
&lt;p&gt;In most languages, spawning a thread involves megabytes of stack memory and significant overhead. A Go goroutine starts with about 2KB of stack. You can run tens of thousands of them on a normal laptop without breaking a sweat.&lt;/p&gt;</description></item><item><title>Lesson 12: Error Handling — Errors are values, not exceptions</title><link>/post/go/go-scratch-errors/</link><pubDate>Wed, 06 Mar 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-errors/</guid><description>&lt;p&gt;The first time I wrote Go after years of Python and JavaScript, I kept waiting for the &lt;code&gt;try/catch&lt;/code&gt; block. It never came. Instead, almost every function returned two values: the result, and an error. I had to check the error every single time. It felt tedious at first. After a few weeks, I realised it was one of the best design decisions in the language.&lt;/p&gt;
&lt;p&gt;In Go, errors are just values. They&amp;rsquo;re not special. They don&amp;rsquo;t teleport up the call stack. They sit right there, in your return value, waiting for you to deal with them.&lt;/p&gt;</description></item><item><title>Lesson 11: Interfaces — Contracts without the paperwork</title><link>/post/go/go-scratch-interfaces/</link><pubDate>Sat, 24 Feb 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-interfaces/</guid><description>&lt;p&gt;When I first learned about interfaces in Go, I expected something complicated — annotations, &lt;code&gt;implements&lt;/code&gt; keywords, class hierarchies. Instead, Go just asked: &amp;ldquo;does your type have the right methods?&amp;rdquo; That&amp;rsquo;s it. No paperwork. No explicit declaration. If your type does what the interface requires, it satisfies the interface automatically.&lt;/p&gt;
&lt;p&gt;This sounds small, but it changes how you design programs entirely.&lt;/p&gt;
&lt;h2 id="the-basics"&gt;The Basics&lt;/h2&gt;
&lt;p&gt;An interface in Go is a named set of method signatures. Any type that implements those methods automatically satisfies the interface. You don&amp;rsquo;t declare that you&amp;rsquo;re satisfying it. Go figures it out at compile time.&lt;/p&gt;</description></item><item><title>Lesson 10: Pointers — Addresses, not magic</title><link>/post/go/go-scratch-pointers/</link><pubDate>Sat, 17 Feb 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-pointers/</guid><description>&lt;p&gt;Pointers have a reputation for being scary. In C, they&amp;rsquo;re the source of buffer overflows, dangling references, and cryptic crashes. In Go, pointers are much tamer. There&amp;rsquo;s no pointer arithmetic, the garbage collector handles memory for you, and the rules are simpler. But pointers are still important — without them, you can&amp;rsquo;t write Go programs that mutate data through function calls, and you can&amp;rsquo;t use pointer receivers (which we covered in the last lesson).&lt;/p&gt;</description></item><item><title>Lesson 9: Methods — Functions that belong to a type</title><link>/post/go/go-scratch-methods/</link><pubDate>Sun, 11 Feb 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-methods/</guid><description>&lt;p&gt;In the last lesson we defined a &lt;code&gt;User&lt;/code&gt; struct. We could pass it to functions — &lt;code&gt;birthday(u)&lt;/code&gt;, &lt;code&gt;sendEmail(u)&lt;/code&gt;, &lt;code&gt;formatName(u)&lt;/code&gt;. That works, but there&amp;rsquo;s a better way: we can attach functions directly to the &lt;code&gt;User&lt;/code&gt; type. Then instead of &lt;code&gt;birthday(u)&lt;/code&gt; we write &lt;code&gt;u.Birthday()&lt;/code&gt;. Those are called methods.&lt;/p&gt;
&lt;p&gt;Methods make code more readable, more organized, and closer to how we naturally think about objects — &amp;ldquo;a user does birthday-related things&amp;rdquo; rather than &amp;ldquo;a birthday function takes a user.&amp;rdquo;&lt;/p&gt;</description></item><item><title>Lesson 8: Structs — Your first custom type</title><link>/post/go/go-scratch-structs/</link><pubDate>Wed, 07 Feb 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-structs/</guid><description>&lt;p&gt;So far we&amp;rsquo;ve been working with Go&amp;rsquo;s built-in types: strings, ints, booleans, slices, maps. Those are great, but real programs deal with real-world things — users, orders, products, messages. You need a way to group related data together and give it a meaningful name. In Go, that&amp;rsquo;s a struct.&lt;/p&gt;
&lt;p&gt;If you come from Python, think of a struct as a lightweight class that holds data (but with no inheritance, and methods are defined separately). If you come from JavaScript, think of it as a typed object. If you come from C, structs work almost exactly the same way.&lt;/p&gt;</description></item><item><title>Lesson 7: Strings, Runes, and Bytes — Strings aren't what you think</title><link>/post/go/go-scratch-strings/</link><pubDate>Fri, 02 Feb 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-strings/</guid><description>&lt;p&gt;Strings look simple. You write &lt;code&gt;&amp;quot;hello&amp;quot;&lt;/code&gt;, you print it, done. But when I first started working with non-ASCII text in Go — names with accents, emoji, Chinese characters — things started behaving oddly. &lt;code&gt;len(&amp;quot;café&amp;quot;)&lt;/code&gt; returned 5, not 4. Iterating with a regular index loop gave me garbled output. It took me a while to understand why, and once I did, a lot of things clicked.&lt;/p&gt;
&lt;p&gt;The short version: Go strings are sequences of bytes, not characters. Once you internalize that, the rest makes sense.&lt;/p&gt;</description></item><item><title>Lesson 6: Maps — Key-value pairs that power everything</title><link>/post/go/go-scratch-maps/</link><pubDate>Sat, 27 Jan 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-maps/</guid><description>&lt;p&gt;Every language has some version of the dictionary or hash map. Python calls it a &lt;code&gt;dict&lt;/code&gt;, JavaScript calls it an &lt;code&gt;Object&lt;/code&gt; or &lt;code&gt;Map&lt;/code&gt;, Ruby calls it a &lt;code&gt;Hash&lt;/code&gt;. In Go, it&amp;rsquo;s just called a map. Once you understand how Go maps work, you&amp;rsquo;ll reach for them constantly — they&amp;rsquo;re one of the most useful data structures in the language.&lt;/p&gt;
&lt;h2 id="the-basics"&gt;The Basics&lt;/h2&gt;
&lt;h3 id="creating-a-map"&gt;Creating a map&lt;/h3&gt;
&lt;p&gt;The zero value of a map is &lt;code&gt;nil&lt;/code&gt;. A nil map is readable (it returns zero values) but not writable. If you try to write to a nil map, your program panics. The safe way to create a map is with &lt;code&gt;make&lt;/code&gt;:&lt;/p&gt;</description></item><item><title>Lesson 5: Arrays and Slices — Slices are what you actually use</title><link>/post/go/go-scratch-arrays-slices/</link><pubDate>Mon, 22 Jan 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-arrays-slices/</guid><description>&lt;p&gt;Go has two sequence types: arrays and slices. I&amp;rsquo;ll tell you upfront — arrays are rarely used directly. They exist, they matter for understanding how things work under the hood, but in day-to-day Go programming you&amp;rsquo;ll almost always reach for a slice instead.&lt;/p&gt;
&lt;p&gt;Slices are Go&amp;rsquo;s workhorse collection type. They&amp;rsquo;re flexible, they grow when you need them to, and they&amp;rsquo;re used everywhere: in standard library APIs, in function parameters, in HTTP handlers. Understanding slices well will make you a much more effective Go programmer than memorizing syntax ever will.&lt;/p&gt;</description></item><item><title>Lesson 4: Functions — Small functions are Go's building blocks</title><link>/post/go/go-scratch-functions/</link><pubDate>Wed, 17 Jan 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-functions/</guid><description>&lt;p&gt;If there&amp;rsquo;s one thing experienced Go programmers agree on, it&amp;rsquo;s this: write small functions. Not tiny, cryptic one-liners, but focused functions that do one thing and are easy to test. Go&amp;rsquo;s function syntax encourages this style — it&amp;rsquo;s clean, explicit, and has one feature that&amp;rsquo;s genuinely different from most languages you&amp;rsquo;ve used before: multiple return values.&lt;/p&gt;
&lt;p&gt;In Python you can return a tuple. In JavaScript you can return an array. But in Go, returning two or three values is a first-class language feature with its own syntax, and it fundamentally changes how error handling works. By the end of this lesson you&amp;rsquo;ll understand Go functions well enough to start writing real, useful programs.&lt;/p&gt;</description></item><item><title>Lesson 3: Control Flow — if, for, and switch are all you need</title><link>/post/go/go-scratch-control-flow/</link><pubDate>Fri, 12 Jan 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-control-flow/</guid><description>&lt;p&gt;Every program you&amp;rsquo;ll ever write needs to make decisions and repeat actions. &amp;ldquo;If this condition is true, do that. Otherwise, do something else.&amp;rdquo; &amp;ldquo;Keep doing this until we run out of items.&amp;rdquo; These are the fundamental building blocks of logic, and Go handles them with just three constructs: &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;, and &lt;code&gt;switch&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s it. No &lt;code&gt;while&lt;/code&gt;. No &lt;code&gt;do-while&lt;/code&gt;. No &lt;code&gt;foreach&lt;/code&gt; as a separate keyword. Go&amp;rsquo;s designers made a deliberate choice to keep the language small — fewer constructs means fewer ways to do the same thing, which means code is more consistent and easier to read across different projects and teams.&lt;/p&gt;</description></item><item><title>Lesson 2: Variables and Types — Go is typed, and that's a good thing</title><link>/post/go/go-scratch-variables-types/</link><pubDate>Mon, 08 Jan 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-variables-types/</guid><description>&lt;p&gt;If you&amp;rsquo;re coming from Python or JavaScript, Go&amp;rsquo;s type system might feel like extra work at first. Why should you have to tell the compiler that a variable holds a number? Can&amp;rsquo;t it just figure it out?&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the thing — it usually can. Go has type inference, so you rarely have to write out types explicitly. But when types &lt;em&gt;are&lt;/em&gt; written out, every person reading your code knows exactly what kind of data they&amp;rsquo;re dealing with. There&amp;rsquo;s no guessing, no surprises at runtime because a string snuck in where you expected a number. Go catches those mistakes at compile time, before your code ever runs.&lt;/p&gt;</description></item><item><title>Lesson 1: Your First Go Program — Hello World is just the beginning</title><link>/post/go/go-scratch-first-program/</link><pubDate>Wed, 03 Jan 2024 00:00:00 +0000</pubDate><guid>/post/go/go-scratch-first-program/</guid><description>&lt;p&gt;I remember the first time I ran a Go program. The compiler yelled at me for an unused import, and I thought, &amp;ldquo;Okay, this language has opinions.&amp;rdquo; That&amp;rsquo;s not a bad thing. Go&amp;rsquo;s strictness is part of what makes it so readable and maintainable. Once you understand &lt;em&gt;why&lt;/em&gt; Go works the way it does, it starts to feel less like friction and more like clarity.&lt;/p&gt;
&lt;p&gt;In this lesson you&amp;rsquo;re going to write your first Go program, understand every single line of it, and learn the basic tools you&amp;rsquo;ll use every day. By the end, &amp;ldquo;Hello, World!&amp;rdquo; won&amp;rsquo;t feel like a throwaway demo — it&amp;rsquo;ll feel like a foundation.&lt;/p&gt;</description></item></channel></rss>