What Changed in Go 1.23’s Timer Implementation and How It Affects Your Code
Go 1.23 introduces a garbage‑collectable, synchronous timer channel implementation that eliminates resource leaks and race conditions, requiring code changes such as replacing len‑checks with non‑blocking selects and offering GODEBUG flags for toggling the new behavior during testing and production.
Go 1.23 adds a new implementation for the channel‑based timers created by time.NewTimer, time.After, time.NewTicker and time.Tick. The change brings two major effects:
Timers and tickers that are stopped but no longer referenced become eligible for garbage collection. Previously, an unstopped timer could not be collected before it fired, and a ticker could never be collected, leading to resource leaks when t.Stop was omitted.
The timer channel is now synchronous (unbuffered). This gives stronger guarantees for t.Reset and t.Stop: after these methods return, a receive on the timer channel will never see a value that belongs to the old configuration. In earlier versions, resetting could leave a stale expiration value, and stopping required careful handling of the return flag.
The new semantics are enabled only when the program’s go.mod declares go 1.23 (or higher) and the package main resides in a module. Older programs keep the old behavior. The GODEBUG variable asynctimerchan=1 forces the legacy implementation, while asynctimerchan=0 forces the new one.
Cap and Len
Before Go 1.23 the timer channel had a capacity of 1 and its length indicated whether a value was waiting (1) or not (0). The new implementation creates a channel with both capacity and length equal to 0, making the channel unbuffered.
Polling a channel with len is generally unsafe because other goroutines may receive concurrently, invalidating the length. The recommended pattern is to replace len‑checks with a non‑blocking select:
if len(t.C) == 1 {
<-t.C
// more code
}should become:
select {
default:
case <-t.C:
// more code
}Select Race
Prior to Go 1.23, timers with extremely short intervals (e.g., 0 ns or 1 ns) suffered scheduling delay, causing the timer’s channel to become ready later than expected. The following program demonstrates the race between a closed channel and a very short‑timeout timer:
c := make(chan bool)
close(c)
select {
case <-c:
println("done")
case <-time.After(1*time.Nanosecond):
println("timeout")
}Because the timer may not have expired when the select evaluates its cases, the closed channel is always ready, so the "done" case runs 100 % of the time. Go 1.23 removes this delay, making the two cases roughly equally likely.
In Google’s codebase, tests that compare a ready context.Done channel with a very short‑timeout timer exhibited this bias. Production code usually uses realistic timeouts, so the issue is mostly visible in tests. A typical failing test looks like:
select {
case <-ctx.Done():
return nil
case <-time.After(timeout):
return errors.New("timeout")
}When timeout is set to 1 ns, the test fails because the timer never fires before the Done case is evaluated. One fix is to prioritize the Done channel after a short timeout:
select {
case <-ctx.Done():
return nil
case <-time.After(timeout):
// In tests with very short timeouts, re‑check Done
select {
default:
case <-ctx.Done():
return nil
}
return errors.New("timeout")
}Debugging
If a program or test passes on Go 1.22 but fails on Go 1.23, you can use the GODEBUG flag to see whether the new timer implementation is the cause:
GODEBUG=asynctimerchan=0 mytest # force Go 1.23 timer
GODEBUG=asynctimerchan=1 mytest # force Go 1.22 timerWhen failures appear only with the new implementation, the next step is to locate the code that depends on the old semantics. The bisect tool can automate this by repeatedly running the test while toggling the timer implementation based on stack traces:
go install golang.org/x/tools/cmd/bisect@latest
bisect -godebug asynctimerchan=1 mytestBisect will narrow down the exact call sites that trigger the new timer behavior and report the relevant stack traces, helping you adjust the code or the test accordingly.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
BirdNest Tech Talk
Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
