That one time I used Go panics for flow control

Lobsters Hottest News

Summary

A Go engineer recounts an incident where an in-memory datastore became overloaded due to slow sorting, and they implemented context cancellation inside sort functions by using panics and recover for non-local flow control, similar to how encoding/json handles errors.

<p><a href="https://lobste.rs/s/arfalj/one_time_i_used_go_panics_for_flow_control">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 05/23/26, 10:43 AM

# That one time I used Go panics for flow control Source: [https://noncrab.net/posts/panic-as-flow-control/](https://noncrab.net/posts/panic-as-flow-control/) How our protagonist discovered that a key service that powers our support was absurdly vulnerable to overload, and what we did to fix it\. Part of our support infrastructure at work is an in\-memory datastore, that allows us to query our outstanding support work over various dimensions, such as work type, whether it's been put on hold for some reason, etc\. It's functionally equivalent to a single table in an SQL database, where you have a single dataset, boolean filters and configurable sorting\. At work, we have an in\-memory datastore that powers part of our support infrastructure\. Its kind of analgous to having bitmap filters with post\-hoc filtering, so any use of sort/limit will sort the entire result set\. And the key part here, is that the result sets can be large enough that sorts can take one or two seconds\. And for a bit of context, this service deployment wasn't autoscaled at the time, and upstream services will retry failed requests\. Sometimes after a relatively short timeout\. Which is fun\. So, one day, this service had more query load than it can handle; and because of the inelasticity, it got overloaded, and queries started to take*way longer*\(like, up to a minute vs\. a typical time of up to 1\-2s\)\. Unfortunately, because this was an incident, and sometimes the panic sets in, one of my theories was that memory had gotten slower\. Which of course was absurd, but under time presssure, incident brain can be very real\. However, as earlier foreshadowed, this service had simply became overloaded, so we not only had slightly higher than average demand, but also*failure*demand from retries\. Most of the time in a Go service, we pass around a[context](https://go.dev/doc/database/cancel-operations), so that when the caller gives up on us, we can cancel the operation, short\-circuit and bail early\. However, when we were able to get a cpu profile and take a look, the vast majority of the CPU time was taken up in the sort phase of the query\. In go, none of the[sort](https://pkg.go.dev/sort)functions support cancellation \(reasonably so, as normally you're either in a batch context, or sorting small enough counts that the time taken isn't significant\)\. So, what to do? Normally, context cancellation has leaf functions[check for an error](https://pkg.go.dev/context#Cause), and then propagate it via the typical[errors\-as\-values mechanism](https://go.dev/doc/tutorial/handle-errors)\. However, none of the sort functions \(eg:[`sort\.Sortfunc`](https://pkg.go.dev/slices#SortFunc)\) take a context, or allow returning an error\. Thankfully, Go has another, non\-local signalling mechanism for handling errors \(eg: if you've dereferenced a`nil`pointer\), in the form of[panics](https://go.dev/blog/defer-panic-and-recover)\. This tends not to be used much for error handling per\-se, because the non\-local flow control can be harded to reason about, but it can make sense within a single narrowly defined context\. For example, the`encoding/json`package does this, for example throwing via[`json\.\(\*encodingState\)\.error\(…\)`](https://github.com/golang/go/blob/go1.26.2/src/encoding/json/encode.go#L348-L350), and recovering within the scope of the top level[`json\.\(\*encodingState\)\.marshal\(…\)`](https://github.com/golang/go/blob/go1.26.2/src/encoding/json/encode.go#L334-L342)function\. So no client code actually sees the non\-local control flow, and no engineers experience unexpected panics\. So we changed the code from something like this: ``` func execute(ctx context.Context) (results, error) { resultSet := query.filter(someTable) slices.SortFunc(resultSet, func(a, b Row) int { return query.compare(a, b) }) } ``` To something like this: ``` type nonLocalCancellation struct {err error} func execute(ctx context.Context) (results, error) { resultSet := query.filter(someTable) var sortErr error defer func() { // Ref: https://go.dev/blog/defer-panic-and-recover if r := recover(); r != nil { if c, ok := r.(nonLocalCancellation); ok { sortErr = c.err } else { panic(r) } } }() slices.SortFunc(resultSet, func(a, b Row) int { if ctx.err return query.compare(a, b) }) if sortErr != nil { return nil, sortErr } return resultSet, nil } ``` Which, is a lot of messing about \(it's an ugly solution to an ugly problem\), but does mean if the caller gives up on the query, we don't waste time sorting a result for someone who will never care about it\.

Similar Articles

Notes from Optimizing CPU-Bound Go Hot Paths

Hacker News Top

The article discusses performance optimization techniques for CPU-bound Go code, highlighting the limitations of generics and interface abstractions due to lack of inlining, and advocates for code duplication in hot paths. It provides examples from a Brotli port and deep-dive benchmarking.

Just Fucking Use Go

Lobsters Hottest

An opinionated developer essay advocates for the Go programming language, emphasizing its straightforward syntax, robust standard library, efficient concurrency model, and single-binary deployment as practical alternatives to overly complex modern technology stacks.

Go Experiments Explained

Lobsters Hottest

This article explains how Go handles experimental features, their lifecycle, and examples of recent experiments.

Tips to debug hanging Go programs

Michael Stapelberg

A practical guide covering three methods to debug hanging Go programs: using SIGQUIT to print stack traces, attaching the delve debugger, and saving core dumps for later analysis.