Inside The Bubble • The Applied Go Weekly Newsletter 2025-06-15
Your weekly source of Go news, tips, and projects
Inside The Bubble
Hi ,
First of all, I'm terribly sorry for publishing this issue late. The weekend was super hot over here, and I wasn't able to concentrate on writing the Spotlight, so I wrote it on Monday. Late, but hopefully not too late!
Speaking of the Spotlight, I wrapped my head around the new synctest
package that has been an experiment in Go 1.24 and will become a first-class member of the standard library in Go 1.25, with only a minor change: The Run()
function got replaced by a function named Test()
that takes the active *testing.T
and passes a modified *testing.T
to the "bubble function". Curious what a bubble function is? Read the Spotlight!
And enjoy your week!
–Christoph
Featured articles
A subtle data race in Go
Puzzle time! Can you find the hidden data race in a tiny piece of apparently concurrency-safe code?
Modern (Go) application design - Office Hours
How do you design Go applications (if you are the one in charge of designing them)? Tit Petric shares his opinions he built during years of Go development.
Go 1.25 Release Candidate 1 is released
It's still a few weeks until August, but you can try and test Go 1.25 now! Release candidate 1 marks the end of the beta phase, which means RC1 is feature-complete and fairly stable, but your load tests and unit tests may help iron out one or another yet undiscovered issue.
[security] Go 1.24.4 and Go 1.23.10 are released
Two security releases are ready for download, fixing issues with net/http
headers, os.OpenFile
and crypto/x509
.
Podcast corner
Fallthrough: A Discourse On AI Discourse
Are LLMs the next big revolution in software development, or are they just overhyped chaos monkeys? Kris, Matt, and this episode's guest Steve Klabnik try to find out.
Spotlight: New in Go 1.25: the synctest package
Go 1.25 is expected to arrive in August. With the release of Go 1.25RC1, the changes to the language, the stdlib, and the toolchain are stable enough to start having a look what's to come.
In this Spotlight, I explore the new package synctest
that helps to test asynchronous behavior.
What is synctest, and how does it work?
Package synctest
introduces two functions, Test()
and Wait()
. The Test()
function executes a function inside an isolated environment called "bubble":
func TestTiming(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
// everything in here belongs to "The Bubble"
})
}
Interesting things happen inside a bubble!
In a nutshell:
Wall-clock time stands still while code executes, unless all goroutines in the bubble are "durably blocked"; that is, they are blocked and cannot be unblocked from outside the bubble.
If all goroutines in the bubble are durably blocked, and the main goroutine is blocked by a call to Wait()
, then Wait()
returns instantly to let the main goroutine continue.
If all goroutines are durably blocked and no Wait()
call is pending, the time instantly advances to the next point in time that unblocks a goroutine, such as the end of a Sleep()
call or the timeout of a Context
.
Only if no Wait()
call or a timing event is pending, the runtime panics.
What does "durably blocked" mean?
A goroutine in a bubble is durably blocked if only another goroutine in the bubble can unblock it. The following operations durably block a goroutine:
- Calling
time.Sleep()
- When a send or receive on a channel created within the bubble blocks
- When a select statement blocks on channels created within the bubble
- Calling
sync.Cond.Wait()
Goroutines are not durably blocked if they
- lock a
sync.(RW)Mutex
- block on system I/O
- do system calls
because all of these can be unblocked from outside the bubble.
What problems does synctest solve?
All the above sounds kinda sophisticated, but what does synctest
buy us?
Testing timing behavior of goroutines has two major problems:
- Slow tests
- Unreliable behavior
Let me look at both in turn.
How synctest speeds up slow tests (tremendously)
Concurrent tests may need to test timeouts or other time-related behavior, as in this test of a context timeout:
func TestTiming(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
start := time.Now()
ctx, _ := context.WithTimeout(t.Context(), 5*time.Second)
<-ctx.Done()
t.Log(time.Since(start))
})
}
The timeout is set to 5 seconds, yet the whole test finishes within a fraction of a second:
> go1.25rc1 test -v
=== RUN TestTiming
synctest_test.go:15: 5s
--- PASS: TestTiming (0.00s)
PASS
ok mod/path/to/synctest 0.153s
How synctest fixes flaky tests through synthetic time
Testing concurrent events can sometimes be non-deterministic. To illustrate this, let me add a test that "proves" that the timeout has not occurred yet.
Also, because a five seconds timeout makes the test terribly slow, I change the timeout to 5 microseconds.
Without the synctest.Test()
harness, the test turns out to generate some errors:
func TestTiming(t *testing.T) {
start := time.Now()
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Microsecond)
defer cancel()
if err := ctx.Err(); err != nil {
t.Fatalf("Should not be timed out yet, got: %v", err)
}
<-ctx.Done()
if err := ctx.Err(); err != context.DeadlineExceeded {
t.Fatalf("Expected DeadlineExceeded, got: %v", err)
}
t.Log(time.Since(start))
}
The following Bash command pipeline runs the test 1000 times, looks for occurrences of got:
, counts them, and prints the deduplicated messages:
> go1.25rc1 test -v -count=1000 | grep 'got:' | tee >(wc -l) | uniq
synctest_test.go:18: Should not be timed out yet, got: context deadline exceeded
9
In nine cases, the test detected a context timeout when it shouldn't.
Add synctest.Test()
back to the test (Playground link) and the same Bash command never lists a single error. The synthetic time concept inside the bubble replaces linearly moving time by a deterministic ordering of timed events.
Shortening the timeout for tests speeds up testing but makes the test more unreliable. The timeout timer, which runs in a separate goroutine, may stop earlier than the main goroutine reaches the end of its Sleep()
call.
In fact, running this test reveals a quite significant count of errors:
> go1.25rc1 test -v -count=1000 | grep 'got:' | tee >(wc -l) | uniq
synctest_test.go:16: Should not be timed out yet, got: context deadline exceeded
575
In more than half of the tests, the context timed out before the main goroutine finished sleeping.
Adding the sycntest
bubble back fixes the problem instantly:
go1.25rc1 test -v -count=1000 | grep 'got:' | tee >(wc -l) | uniq
0
I could even change the timeout back to 5 seconds and still run 1,000 tests in seconds.
How synctest fixes flaky tests with Wait()
Now let's change the test a bit: I want to verify if the timeout happens after the specified duration. Inside the bubble, the synthetic time allows my test to advance the time to just one nanosecond before the timeout. Then I add a Wait()
call to ensure the test runs only if all other goroutines (read: the timer's goroutine) are durably blocked and no concurrent activity is going on.
Then the code advances by another nanosecond and calls Wait() again, to ensure the timer (that should have triggered by then) has ceased any concrrent activity before testing the context error.
The resulting code looks like this:
func TestTiming(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
start := time.Now()
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
time.Sleep(5*time.Second - time.Nanosecond)
synctest.Wait()
if err := ctx.Err(); err != nil {
t.Fatalf("Should not be timed out yet, got: %v", err)
}
time.Sleep(time.Nanosecond)
synctest.Wait()
if err := ctx.Err(); err != context.DeadlineExceeded {
t.Fatalf("Expected DeadlineExceeded, got: %v", err)
}
t.Log(time.Since(start))
})
}
The result is reliably zero errors, even with 100,000 iterations:
go1.25rc1 test -v -count=100000 | grep 'got:' | tee >(wc -l) | uniq
0
How to use synctest at the moment
Until Go 1.25 is out, there are three ways of trying out synctest:
- In the Go playground, set the Go version to "Go dev branch"
- Download the latest release candidate of Go 1.25 (here's how)
- With Go 1.24, use the environment setting
GOEXPERIMENT=syntest
(1)
(1) Note that in Go 1.24, you need to use the now-deprecated synctest.Run()
instead of synctest.Test(*testing.T, func(*testing.T))
.
Quote of the Week: They Succeeded Nonetheless
I’m confident the designers of the Go programming language didn’t set out to produce the most LLM-legible language in the industry. They succeeded nonetheless.
More articles, videos, talks
What should your mutexes be named?
How do you name your mutexes? mu
, or <var>Mux
(where is the name of the variable the mutex protects), or something else? Philippe Gaultier decided to let the statistics decide.
Bifrost: A Drop-in LLM Proxy, 40x Faster Than LiteLLM
When you tinker with LLMs, you might reach a point where connecting all your LLM clients/frontends (Open WebUI, AnythingLLM, you name them) to all the LLMs you have API keys for becomes tedious. An LLM proxy acts as a single access point for your LLM clients, so that you need to configure API urls and access keys only once—inside the LLM proxy. If you know LiteLLM or OpenRouter, you know what Bifrost is.
Why I Made Peace With Go’s Date Formatting · Preslav Rachev
Like Preslav Rachev, I wasn't happy with Go's quite individual date&time formatting. Now, I see it like him: A date and time format string might be more cumbersome to write compared to other languages' stdlibs. But on the other hand, the final format string is much easier to read.
NebuLeet on Steam
A space shooter made with Ebitengine. No source code, unfortunately, as the game is a commercial product; however, the announcement spawned an engaged discussion about game development in /r/golang
.
Ebitengine Game Jam 2025 - itch.io
The Ebitengine Game Jam 2025 starts... today! The day this newsletter issue went out, the gates open until June 29th to submit your Ebitengine-based game built upon a given theme.
Projects
Libraries
GitHub - jkaninda/okapi: OKAPI - Lightweight Minimalist Go Web Framework with OpenAPI 3.0 & Swagger UI
"Designed for simplicity, performance, and developer happiness" (emphasis mine)
GitHub - bernardinorafael/go-boilerplate: A boilerplate for starting Go projects
Tired of writing boilerplate code? Here is boilerplate code written for you!
GitHub - RezaSi/go-interview-practice: Go Interview Practice is a series of coding challenges to help you prepare for technical interviews in Go. Solve problems, submit your solutions, and receive instant feedback with automated testing. Track your progress with per-challenge scoreboards and improve your coding skills step by step.
Go challenges for interview preparation, for learning, or just for fun.
Tools and applications
GitHub - fillmore-labs/cmplint: cmplint is a Go linter that detects comparisons against the address of newly created values.
Comparing a pointer against the address of a newly created variable, such as in if errors.Is(err, &url.Error{})
, is almost always wrong. cmplint
helps to detect and prevent these kinds of errors.
GitHub - madalinpopa/gocost: Simple TUI application to manage monthly expenses
Track your money in the terminal.
GitHub - nobrainghost/golamv2: Lightweight Web Crawler for Emails,Keywords,Deadlinks,Dead Domains written in Go. Suitable for low resource environments
Crawl web pages for emails, keywords, or dead links. GoIamV2 respects robots.txt and runs on low resources.
GitHub - piyushgupta53/go-torrent-client: A BitTorrent client implementation in Go
A hobby project built by a self-taught programmer. Not bad!
GitHub - cvsouth/go-package-analyzer: A simple tool to analyze and visualize Go package dependencies.
Analyze your package dependencies interactively in your browser.
GitHub - m-ocean-it/correcterr
Using one error in the if
condition and returning another error in the if
block isn't wise. The linter correcterr
catches this particular mistake.
Completely unrelated to Go
Apple just Sherlocked Docker
Apple works on a container system for Apple Silicon machines. And it's open source. Xe Iaso hopes it helps to get a few CPU cycles back.

Happy coding! ʕ◔ϖ◔ʔ
Questions or feedback? Drop me a line. I'd love to hear from you.
Best from Munich, Christoph
Not a subscriber yet?
If you read this newsletter issue online, or if someone forwarded the newsletter to you, subscribe for regular updates to get every new issue earlier than the online version, and more reliable than an occasional forwarding.
Find the subscription form at the end of this page.
How I can help
If you're looking for more useful content around Go, here are some ways I can help you become a better Gopher (or a Gopher at all):
On AppliedGo.net, I blog about Go projects, algorithms and data structures in Go, and other fun stuff.
Or visit the AppliedGo.com blog and learn about language specifics, Go updates, and programming-related stuff.
My AppliedGo YouTube channel hosts quick tip and crash course videos that help you get more productive and creative with Go.
Enroll in my Go course for developers that stands out for its intense use of animated graphics for explaining abstract concepts in an intuitive way. Numerous short and concise lectures allow you to schedule your learning flow as you like.
Christoph Berger IT Products and Services
Dachauer Straße 29
Bergkirchen
Germany