16.6 Combining select with for: The Event Loop Pattern

Right, so you’ve met select and you’ve met for. Individually, they’re useful. Together, they form the bedrock of most concurrent Go programs you’ll ever write. This is the pattern you use when you need to manage multiple communication operations—waiting on several channels at once without knowing which will be ready first. It’s the event loop for the pragmatic gopher. The basic idea is devilishly simple: you wrap a select statement inside an infinite for loop. This lets you continuously listen for events—messages coming in, signals to shut down, timers expiring—and handle them as they arrive, all within a single goroutine.

16.5 Done Channel Pattern: Cancellation with select

Now, let’s talk about one of the most genuinely useful patterns you’ll employ with select: using a done channel for clean and responsive cancellation. This is the pattern that saves you from the dreaded “my goroutine is stuck forever” scenario, and it’s so idiomatic it might as well be Go’s official way of saying “stop what you’re doing.” The core idea is simple yet brilliant. You pass a <-chan struct{} (typically named doneCtx or ctx.Done()) into a goroutine. This channel will never receive any meaningful data; its sole purpose is to be closed when it’s time to shut things down. The goroutine then uses a select statement to simultaneously listen for its normal work and this cancellation signal. When the done channel is closed, the case for it is immediately selected (because receiving from a closed channel always returns the zero value), and the goroutine can bail out. It’s a fire alarm, not a mail slot.

16.4 Non-Blocking Channel Operations with default

Right, so you’ve got your select block primed and ready to listen on a bunch of channels. But what happens when none of them are ready? By default, select will block, sitting there patiently (or, let’s be honest, completely uselessly) until at least one of its cases can proceed. That’s often what you want, but sometimes you need to do something else while you’re waiting. You need a way to peek at the channels and say, “Nothing? Alright, I’ll go do something useful and check back in a bit.” That’s where the default case comes in.

16.3 Implementing Timeouts with time.After

Now, let’s talk about saving your select statements from hanging around forever like a bad party guest. You’ve got a goroutine waiting on a channel, and you’re thinking, “What if that signal never comes?” Enter time.After. This isn’t just a function; it’s your get-out-of-jail-free card for channel operations. The time.After function returns a channel (<-chan Time). You don’t get to send on it; you only get to receive from it. After the duration you specify, the runtime will send the current time on that channel. Exactly once. It’s a one-shot deal. The real magic happens when you drop this channel into a select statement alongside your other cases. It gives the entire operation a hard deadline.

16.2 Randomized Selection When Multiple Cases Are Ready

Right, so you’ve got a select statement with multiple cases that are all ready to fire at the same time. What happens? Chaos? Anarchy? A coin flip in the heart of the Go runtime? Precisely. It’s a coin flip. This is one of those beautiful, pragmatic, and occasionally infuriating design choices Go makes. The language spec doesn’t dictate a strict, predictable order. Instead, when multiple cases in a select are ready to proceed—meaning multiple channels have data to receive or are ready to send—one is chosen pseudo-randomly. I say “pseudo-randomly” because it’s not truly random (it’s deterministic from the runtime’s perspective), but from your code’s perspective, it’s effectively random. You can’t predict it.

16.1 select: Waiting on Multiple Channel Operations

Right, so you’ve got goroutines firing off left and right, channels shuttling data all over the place. It’s beautiful chaos. But what happens when you need to listen to more than one channel at a time? You can’t just sit on a single <-ch receive operation; that’s like trying to listen to two conversations by putting your ear to one person’s mouth. You need a better tool. You need select.

14.6 Keyset Pagination: The Scalable Alternative to OFFSET

Right, let’s talk about pagination. You’ve probably done the OFFSET 100 LIMIT 10 dance a thousand times. It’s the default. It’s also a performance nightmare waiting to happen, and I’m going to show you exactly why. The problem with OFFSET is that it’s a dumb skip. The database has to read the first 100 rows, count them, and then throw them away to give you the 10 you actually want. It’s like asking a librarian to count out the first 100 books in the library, then hand you the 101st. It gets slower and more expensive the deeper you paginate. It’s absurd.

14.5 DISTINCT and DISTINCT ON: Deduplication Patterns

Right, so you’ve fetched some data and it looks… repetitive. Maybe you asked for a list of departments and got back 10,000 rows because you pulled it from the employees table. This is where DISTINCT waltzes in, ready to clean up the mess. It’s the SQL keyword for “get unique values,” and it does exactly what it says on the tin. But like any simple tool, it has hidden depths and a slightly more powerful, albeit quirky, cousin: DISTINCT ON. Let’s get into it.

14.4 LIMIT and OFFSET: Pagination and Its Scalability Issues

Right, let’s talk about LIMIT and OFFSET. You’re going to use them constantly, and you’re going to hate them eventually. They are the duct tape and baling wire of pagination—it gets the job done in a pinch, but you wouldn’t want to build a skyscraper with it. They feel intuitive: “just give me 10 results, but skip the first 20.” And for small datasets, that’s perfectly fine. But the moment your data grows, this approach reveals its fundamental, almost comical, flaw.

14.3 ORDER BY: ASC/DESC, NULLS FIRST/LAST, Multiple Sort Keys

Alright, let’s talk about ORDER BY. This is where you stop just grabbing data and start imposing some actual order on the chaos. It’s the difference between a pile of ingredients and a prepared meal. The ORDER BY clause is your way of telling the database, “I don’t just want the data; I want it like this.” The basic syntax is brain-dead simple: ORDER BY column_name. But the devil, and the real power, is in the details.

14.2 WHERE: Comparison Operators, BETWEEN, IN, IS NULL, LIKE

Alright, let’s talk about the WHERE clause. This is where you stop just fetching data and start interrogating it. Think of SELECT as your “grab everything” command. WHERE is the part where you say, “…but only the stuff that meets these conditions.” It’s the bouncer at the club of your database, deciding which rows get to come in and which get left out in the cold. The magic of WHERE is in its operators. They’re the vocabulary you use to describe exactly what you’re looking for.

14.1 SELECT Column List, Aliases, and Expression Columns

Right, let’s talk about the part of the SELECT statement that feels the most like creative writing: the column list. This is where you tell the database exactly what you want to see, and it’s far more powerful than just listing a few column names. You can rename things on the fly, perform calculations, and even create entirely new data from what’s stored. It’s the projection of your data universe.

— joke —

...