Wrapping a gRPC client in Go

· 9 min

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.

Mutate your locked state inside a closure

· 7 min

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:

Your Go tests probably don't need a mocking library

· 16 min

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.

Revisiting interface segregation in Go

· 6 min

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.

Avoiding collisions in Go context keys

· 8 min

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.

Let the domain guide your application structure

· 6 min

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.

Lifecycle management in Go tests

· 8 min

Unlike pytest or JUnit, Go’s standard testing framework doesn’t give you as many knobs for tuning the lifecycle of your tests.

By lifecycle I mean the usual setup and teardown hooks or fixtures that are common in other languages. I think this is a good thing because you don’t need to pick up many different framework-specific workflows for something so fundamental.

Go gives you enough hooks to handle this with less ceremony. But it can still be tricky to figure out the right conventions for setup and teardown that don’t look odd to other Gophers, especially if you haven’t written Go for a while. This text explores some common ways to do lifecycle management in your Go tests.

You probably don't need a DI framework

· 8 min

When working with Go in an industrial programming context, I feel like dependency injection (DI) often gets a bad rep because of DI frameworks. But DI as a technique is quite useful. It just tends to get explained with too many OO jargons and triggers PTSD among those who came to Go to escape GoF theology.

Dependency Injection is a 25-dollar term for a 5-cent concept.

– James Shore

DI basically means passing values into a constructor instead of creating them inside it. That’s really it. Observe:

Deferred teardown closure in Go testing

· 6 min

While watching Mitchell Hashimoto’s Advanced Testing with Go talk, I came across this neat technique for deferring teardown to the caller. Let’s say you have a helper function in a test that needs to perform some cleanup afterward.

You can’t run the teardown inside the helper itself because the test still needs the setup. For example, in the following case, the helper runs its teardown immediately:

func TestFoo(t *testing.T) {
    helper(t)

    // Test logic here: resources may already be cleaned up!
}

func helper(t *testing.T) {
    t.Helper()

    // Setup code here.

    // Teardown code here.
    defer func() {
        // Clean up something.
    }()
}

When helper is called, it defers its teardown - which executes at the end of the helper function, not the test. But the test logic still depends on whatever the helper set up. So this approach doesn’t work.

Stacked middleware vs embedded delegation in Go

· 5 min

Middleware is usually the go-to pattern in Go HTTP servers for tweaking request behavior. Typically, you wrap your base handler with layers of middleware - one might log every request, while another intercepts specific routes like /special to serve a custom response.

However, I often find the indirections introduced by this pattern a bit hard to read and debug. I recently came across the embedded delegation pattern while browsing Gin’s HTTP router source code. Here, I explore both patterns and explain why I usually start with delegation whenever I need to modify HTTP requests in my Go services.

Function types and single-method interfaces in Go

· 6 min

People love single-method interfaces (SMIs) in Go. They’re simple to implement and easy to reason about. The standard library is packed with SMIs like io.Reader, io.Writer, io.Closer, io.Seeker, and more.

One cool thing about SMIs is that you don’t always need to create a full-blown struct with a method to satisfy the interface. You can define a function type, attach the interface method to it, and use it right away. This approach works well when there’s no state to maintain, so the extra struct becomes unnecessary. However, I find the syntax for this a bit abstruse. So, I’m jotting down a few examples here to reference later.

Shades of testing HTTP requests in Python

· 5 min

Here’s a Python snippet that makes an HTTP POST request:

# script.py

import httpx
from typing import Any


async def make_request(url: str) -> dict[str, Any]:
    headers = {"Content-Type": "application/json"}

    async with httpx.AsyncClient(headers=headers) as client:
        response = await client.post(
            url,
            json={"key_1": "value_1", "key_2": "value_2"},
        )
        return response.json()

The function make_request makes an async HTTP request with the HTTPx library. Running this with asyncio.run(make_request("https://httpbin.org/post")) gives us the following output:

{
  "args": {},
  "data": "{\"key_1\": \"value_1\", \"key_2\": \"value_2\"}",
  "files": {},
  "form": {},
  "headers": {
    "Accept": "*/*",
    "Accept-Encoding": "gzip, deflate",
    "Content-Length": "40",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "python-httpx/0.27.2",
    "X-Amzn-Trace-Id": "Root=1-66d5f7b0-2ed0ddc57241f0960f28bc91"
  },
  "json": {
    "key_1": "value_1",
    "key_2": "value_2"
  },
  "origin": "95.90.238.240",
  "url": "https://httpbin.org/post"
}

We’re only interested in the json field and want to assert in our test that making the HTTP call returns the expected values.

Log context propagation in Python ASGI apps

· 7 min

Let’s say you have a web app that emits log messages from different layers. Your log shipper collects and sends these messages to a destination like Datadog where you can query them. One common requirement is to tag the log messages with some common attributes, which you can use later to query them.

In distributed tracing, this tagging is usually known as context propagation, where you’re attaching some contextual information to your log messages that you can use later for query purposes. However, if you have to collect the context at each layer of your application and pass it manually to the downstream ones, that’d make the whole process quite painful.

Protobuffed contracts

· 4 min

People typically associate Google’s Protocol Buffer with gRPC services, and rightfully so. But things often get confusing when discussing protobufs because the term can mean different things:

  • A binary protocol for efficiently serializing structured data.
  • A language used to specify how this data should be structured.

In gRPC services, you usually use both: the protobuf language in proto files defines the service interface, and then the clients use the same proto files to communicate with the services.

ETag and HTTP caching

· 7 min

One neat use case for the HTTP ETag header is client-side HTTP caching for GET requests. Along with the ETag header, the caching workflow requires you to fiddle with other conditional HTTP headers like If-Match or If-None-Match. However, their interaction can feel a bit confusing at times.

Every time I need to tackle this, I end up spending some time browsing through the relevant MDN docs on ETag, If-Match, and If-None-Match to jog my memory. At this point, I’ve done it enough times to justify spending the time to write this.