Edit Distance from the previous lesson is the template for a whole family of problems. Any time you’re comparing two strings — finding common parts, matching patterns, counting transformations — you’re drawing a 2D table where one string indexes the rows and the other indexes the columns.
The structure is always the same:
dp[i][j]answers a question about the first i characters of one string and the first j characters of the other. The recurrence depends on whether the current characters match and what operations are allowed.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.
If you’re coming from Python, Java, or JavaScript, Go’s error handling will feel strange at first. There’s no
try/catch. 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’t crash loudly; it quietly lies to you, and you won’t find out until 2am when production is on fire.The Problem
The blank identifier
_is the most dangerous character in Go. Here’s what it looks like when engineers first start writing Go:1D DP was a single row — you filled it left to right and you were done. 2D DP extends that to a full table. You fill it row by row, and each cell depends on cells above it, to its left, or diagonally above-left. The shape of that dependency is the shape of the problem.
I find 2D DP more intuitive than 1D once I got comfortable with the table visualization. Unique Paths is a perfect first problem because you can literally draw the grid and fill it in by hand in 30 seconds. Edit Distance is the crown jewel — once you understand why the three transitions correspond to the three edit operations, you’ll never forget the recurrence.
An agent is a loop: observe, think, act, repeat. The LLM is the “think” step — it decides what to do next given the current state. Your Go code handles “observe” (gathering context), “act” (executing tool calls), and the loop control that keeps everything running. I’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’s concurrency makes the execution layer clean and fast.
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 “we didn’t expect this load.” The real cause: no worker pool.
I spent a long time reaching for third-party database libraries in Go before I actually read the
database/sqldocs. When I finally did, I was embarrassed — the stdlib had everything I needed and I’d been adding dependencies for no reason. The problem isn’t thatdatabase/sqlis limited. The problem is that it has a handful of non-obvious behaviors that, if you don’t know about them, will burn you in production. Once you internalize those, you’ll write better database code than most people using ORMs.String concatenation with
+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.result += pieceis obvious. The quadratic memory behavior that follows is not.The
stringsandbytespackages are where Go provides the right tools for this job.strings.Builderandbytes.Bufferare both efficient for incremental string construction, but they’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:strings.Containsvsstrings.Index,strings.Splitvsstrings.SplitN, and the ones people reach for that they shouldn’t.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.
Every time you open a file, acquire a lock, or start a database transaction, you’ve created a resource that needs to be released when you’re done with it. In most languages you manage this with
finallyblocks or RAII patterns. Miss one cleanup and you’ve got a leak. Go hasdefer, and once it clicks, you’ll wonder how you ever coded without it.The Problem
Here’s what resource cleanup looks like without
defer. Real code, the kind that ships: