Testing unary gRPC services in Go

We don’t want to test gRPC or an HTTP server itself, we simply want to test our method’s logic. The simple answer to this question is to de-couple gRPC’s work from the actual work. – John Doak, Testing gRPC methods That advice is right most of the time. If your handler is a thin shell over business logic that lives behind an interface, you can test the logic without gRPC at all. Inject a fake, call the method, check the result. ...

March 23, 2026

Repositories, transactions, and unit of work in Go

This post started as a pair of quick answers to questions on r/golang. The first was about whether a repository layer on top of sqlc is worth it. The second was about how to handle transactions when the interface hides storage details. Both turned into short shards on this site. This post ties them together and covers what to do when transactions need to span multiple repositories. It walks through three stages, each building on the last: ...

March 21, 2026

Wrapping a gRPC client in Go

Yesterday I wrote a shard on exploring the etcd codebase. One of the things that stood out was how the clientv3 package abstracts out the underlying gRPC machinery. etcd is a distributed key-value store where the server and client communicate over gRPC. But if you’ve only ever used clientv3 and never peeked into the internals, you wouldn’t know that. You call resp, err := client.Put(ctx, "key", "value") and get back a *PutResponse. It feels like a regular Go library. The fact that gRPC and protobuf are involved is an implementation detail that the client wrapper keeps away from you. ...

March 15, 2026

Go errors: to wrap or not to wrap?

A lot of the time, the software I write boils down to three phases: parse some input, run it through a state machine, and persist the result. In this kind of code, you spend a lot of time knitting your error path, hoping that it’d be easier to find the root cause during an incident. This raises the following questions: When to fmt.Errorf("doing X: %w", err) When to use %v instead of %w When to just return err There’s no consensus, and the answer changes depending on the kind of application you’re writing. The Go 1.13 blog already covers the mechanics and offers some guidance, but I wanted to collect more evidence of what people are actually doing in the open and share what’s worked for me. ...

March 7, 2026

Mutate your locked state inside a closure

When multiple goroutines need to read and write the same value, you need a mutex to make sure they don’t step on each other. Without one, concurrent writes can corrupt the state - two goroutines might read the same value, both modify it, and one silently overwrites the other’s change. The usual approach is to put a sync.Mutex next to the fields it protects: var ( mu sync.Mutex counter int ) mu.Lock() counter++ mu.Unlock() This works, but nothing enforces it. The compiler won’t stop you from accessing counter without holding the lock. Forget to lock in one spot and you have a data race. One way to make this safer is to bundle the value and its mutex into a small generic wrapper that only exposes locked access through methods: ...

March 5, 2026

What canceled my Go context?

I’ve spent way more hours than I’d like to admit debugging context canceled and context deadline exceeded errors. These errors usually tell you that a context was canceled, but not exactly why. In a typical client-server scenario, the reason could be any of the following: The client disconnected A parent deadline expired The server started shutting down Some code somewhere called cancel() explicitly Go 1.20 and 1.21 added cause-tracking functions to the context package that fix this, but there’s a subtlety with WithTimeoutCause that most examples skip. ...

February 24, 2026

Structured concurrency & Go

At my workplace, a lot of folks are coming to Go from Python and Kotlin. Both languages have structured concurrency built into their async runtimes, and people are often surprised that Go doesn’t. The go statement just launches a goroutine and walks away. There’s no scope that waits for it, no automatic cancellation if the parent dies, no built-in way to collect its errors. This post looks at where the idea of structured concurrency comes from, what it looks like in Python and Kotlin, and how you get the same behavior in Go using errgroup, WaitGroup, and context. ...

February 21, 2026

Your Go tests probably don't need a mocking library

There are frameworks that generate those kind of fakes, and one of them is called GoMock… they’re fine, but I find that on balance, the handwritten fakes tend to be easier to reason about and clearer to sort of see what’s going on. But I’m not an enterprise Go programmer. Maybe people do need that, so I don’t know, but that’s my advice. – Andrew Gerrand, Testing Techniques (46:44) No shade against mocking libraries like gomock or mockery. I use them all the time, both at work and outside. But one thing I’ve noticed is that generating mocks often leads to poorly designed tests and increases onboarding time for a codebase. ...

January 23, 2026

Splintered failure modes in Go

A man with a watch knows what time it is. A man with two watches is never sure. – Segal’s Law Take this example: func validate(input string) (bool, error) { // Validation check 1 if input == "" { return false, nil } // Validation check 2 if isCorrupted(input) { return false, nil } // System check if err := checkUpstream(); err != nil { return false, err } return true, nil } This function returns two signals: a boolean to indicate if the string is valid, and an error to explain any problem the function might run into. ...

November 30, 2025

Re-exec testing Go subprocesses

When testing Go code that spawns subprocesses, you usually have three options. Run the real command. It invokes the actual binary that creates the subprocess and asserts against the output. However, that makes tests slow and tied to the environment. You have to make sure the same binary exists and behaves the same everywhere, which is harder than it sounds. Fake it. Mock the subprocess to keep tests fast and isolated. The problem is that the fake version doesn’t behave like a real process. It won’t fail, write to stderr, or exit with a non-zero code. That makes it hard to trust the result, and over time the mock can drift away from what the real command actually does. ...

November 16, 2025

Revisiting interface segregation in Go

Object-oriented (OO) patterns get a lot of flak in the Go community, and often for good reason. Still, I’ve found that principles like SOLID, despite their OO origin, can be useful guides when thinking about design in Go. Recently, while chatting with a few colleagues new to Go, I noticed that some of them had spontaneously rediscovered the Interface Segregation Principle (the “I” in SOLID) without even realizing it. The benefits were obvious, but without a shared vocabulary, it was harder to talk about and generalize the idea. ...

November 1, 2025

Avoiding collisions in Go context keys

Along with propagating deadlines and cancellation signals, Go’s context package can also carry request-scoped values across API boundaries and processes. There are only two public API constructs associated with context values: func WithValue(parent Context, key, val any) Context func (c Context) Value(key any) any WithValue can take any comparable value as both the key and the value. The key defines how the stored value is identified, and the value can be any data you want to pass through the call chain. ...

October 22, 2025

Organizing Go tests

When it comes to test organization, Go’s standard testing library only gives you a few options. I think that’s a great thing because there are fewer details to remember and fewer things to onboard people to. However, during code reviews, I often see people contravene a few common conventions around test organization, especially those who are new to the language. If we distill the most common questions that come up when organizing tests, they are: ...

October 8, 2025

Subtest grouping in Go

Go has support for subtests starting from version 1.7. With t.Run, you can nest tests, assign names to cases, and let the runner execute work in parallel by calling t.Parallel from subtests if needed. For small suites, a flat set of t.Run calls is usually enough. That’s where I tend to begin. As the suite grows, your setup and teardown requirements may demand subtest grouping. There are multiple ways to handle that. ...

October 1, 2025

Let the domain guide your application structure

I like to make the distinction between application structure and architecture. Structure is how you organize the directories and packages in your app while architecture is how different components talk to each other. The way your app talks to other services in a fleet can also be called architecture. While structure often influences architecture and vice versa, this distinction is important. This post is strictly about application structure and not library structure. Library structure is often driven by different design pressures than their app counterparts. There are a ton of canonical examples of good library structure in the stdlib, but it’s app structure where things get a bit more muddy. ...

September 20, 2025