Omitting dev dependencies in Go binaries

· 3 min

As of now, unlike Python or NodeJS, Go doesn’t allow you to specify your development dependencies separately from those of the application. However, I like to specify the dev dependencies explicitly for better reproducibility.

While working on link-patrol, a CLI for checking dead URLs in Markdown files, I came across this neat convention: you can specify dev dependencies in a tools.go file and then exclude them while building the binary using a build tag.

Annotating args and kwargs in Python

· 3 min

While I tend to avoid *args and **kwargs in my function signatures, it’s not always possible to do so without hurting API ergonomics. Especially when you need to write functions that call other helper functions with the same signature.

Typing *args and **kwargs has always been a pain since you couldn’t annotate them precisely before. For example, if all the positional and keyword arguments of a function had the same type, you could do this:

Statically enforcing frozen data classes in Python

· 3 min

You can use @dataclass(frozen=True) to make instances of a data class immutable during runtime. However, there’s a small caveat - instantiating a frozen data class is slightly slower than a non-frozen one. This is because, when you enable frozen=True, Python has to generate __setattr__ and __delattr__ methods during class definition time and invoke them for each instantiation.

Below is a quick benchmark comparing the instantiation times of a mutable dataclass and a frozen one (in Python 3.12):

Debugging dockerized Python apps in VSCode

· 4 min

Despite using VSCode as my primary editor, I never really bothered to set up the native debugger to step through application code running inside Docker containers. Configuring the debugger to work with individual files, libraries, or natively running servers is straightforward. So, I use it in those cases and just resort back to my terminal for debugging containerized apps running locally. However, after seeing a colleague’s workflow in a pair-programming session, I wanted to configure the debugger to cover this scenario too.

Dotfile stewardship for the indolent

· 4 min

I’m one of those people who will sit in front of a computer for hours, fiddling with algorithms or debugging performance issues, yet won’t spend 10 minutes to improve their workflows. While I usually get away with this, every now and then, my inertia slithers back to bite me. The latest episode was me realizing how tedious it is to move config files across multiple devices when I was configuring a new MacBook Air and Mac Mini at the same time.

Self-hosted Google Fonts in Hugo

· 2 min

This site is built with Hugo and served via GitHub Pages. Recently, I decided to change the font here to make things more consistent across different devices. However, I didn’t want to go with Google Fonts for a few reasons:

  • CDN is another dependency.
  • Hosting static assets on GitHub Pages has served me well.
  • Google Fonts tracks users and violates GDPR in Germany. Google Analytics does that too. But since I’m using the latter anyway, this might come off a bit apocryphal.
  • I wanted to get a few extra Lighthouse points.

Turns out, it’s pretty easy to host the fonts yourself.

Dummy load balancer in a single Go script

· 7 min

I was curious to see if I could prototype a simple load balancer in a single Go script. Go’s standard library and goroutines make this trivial. Here’s what the script needs to do:

  • Spin up two backend servers that’ll handle the incoming requests.
  • Run a reverse proxy load balancer in the foreground.
  • The load balancer will accept client connections and round-robin them to one of the backend servers; balancing the inbound load.
  • Once a backend responds, the load balancer will relay the response back to the client.
  • For simplicity, we’ll only handle client’s GET requests.

Obviously, this won’t have SSL termination, advanced balancing algorithms, or session persistence like you’d get with Nginx or Caddy. The point is to understand the basic workflow and show how Go makes it easy to write this sort of stuff.

Limit goroutines with buffered channels

· 5 min

I was cobbling together a long-running Go script to send webhook messages to a system when some events occur. The initial script would continuously poll a Kafka topic for events and spawn worker goroutines in a fire-and-forget manner to make HTTP requests to the destination. This had two problems:

  • It could create unlimited goroutines if many events arrived quickly (no backpressure)
  • It might overload the destination system by making many concurrent requests (no concurrency control)

In Python, I’d use just asyncio.Semaphore to limit concurrency. I’ve previously written about limiting concurrency with semaphores. Turns out, in Go, you could do the same with a buffered channel. Here’s how the naive version without any concurrency control looks:

Writing a TOTP client in Go

· 3 min

A TOTP based 2FA system has two parts. One is a client that generates the TOTP code. The other part is a server. The server verifies the code. If the client and the server-generated codes match, the server allows the inbound user to access the target system. The code usually expires after 30 seconds and then, you’ll have to regenerate it to be able to authenticate.

As per RFC-6238, the server shares a base-32 encoded secret key with the client. Using this shared secret and the current UNIX timestamp, the client generates a 6-digit code. Independently, the server also generates a 6-digit code using the same secret string and its own current timestamp. If the user-entered client code matches the server-generated code, the auth succeeds. Otherwise, it fails. The client’s and the server’s current timestamp wouldn’t be an exact match. So the algorithm usually adjusts it for ~30 seconds duration.

Interface guards in Go

· 2 min

I love Go’s implicit interfaces. While convenient, they can also introduce subtle bugs unless you’re careful. Types expected to conform to certain interfaces can fluidly add or remove methods. The compiler will only complain if an identifier anticipates an interface, but is passed a type that doesn’t implement that interface. This can be problematic if you need to export types that are required to implement specific interfaces as part of their API contract.