Go 1.25 is scheduled for release in August, so it's a good time to explore what's new. The official release notes are pretty dry, so I prepared an interactive version with lots of examples showing what has changed and what the new behavior is.
Read on and see!
synctest • json/v2 • GOMAXPROCS • New GC • Anti-CSRF • WaitGroup.Go • FlightRecorder • os.Root • reflect.TypeAssert • T.Attr • slog.GroupAttrs • hash.Cloner
This article is based on the official release notes from The Go Authors, licensed under the BSD-3-Clause license. This is not an exhaustive list; see the official release notes for that.
I provide links to the proposals (𝗣) and commits (𝗖𝗟) for the features described. Check them out for motivation and implementation details.
Error handling is often skipped to keep things simple. Don't do this in production ツ
# Synthetic time for testing
Suppose we have a function that waits for a value from a channel for one minute, then times out:
We use it like this:
How do we test the timeout situation? Surely we don't want the test to actually wait 60 seconds. We could make the timeout a parameter (we probably should), but let's say we prefer not to.
The new synctest package to the rescue! The synctest.Test function executes an isolated "bubble". Within the bubble, time package functions use a fake clock, allowing our test to pass instantly:
The initial time in the bubble is midnight UTC 2000-01-01. Time advances when every goroutine in the bubble is blocked. In our example, when the only goroutine is blocked on select in Read, the bubble's clock advances 60 seconds, triggering the timeout case.
Keep in mind that the t passed to the inner function isn't exactly the usual testing.T. In particular, you should never call T.Run, T.Parallel, or T.Deadline on it:
So, no inner tests inside the bubble.
Another useful function is synctest.Wait. It waits for all goroutines in the bubble to block, then resumes execution:
Try commenting out the Wait() call and see how the innerStarted value changes.
The testing/synctest package was first introduced as experimental in version 1.24. It's now considered stable and ready to use. Note that the Run function, which was added in 1.24, is now deprecated. You should use Test instead.
𝗣 67434, 73567 • 𝗖𝗟 629735, 629856, 671961
# JSON v2
The second version of the json package is a big update, and it has a lot of breaking changes. That's why I wrote a separate post with a detailed review of what's changed and lots of interactive examples.
Here, I'll just show one of the most impressive features.
With json/v2 you're no longer limited to just one way of marshaling a specific type. Now you can use custom marshalers and unmarshalers whenever you need, with the generic MarshalToFunc and UnmarshalFromFunc functions.
For example, we can marshal boolean values (true/false) and "boolean-like" strings (on/off) to ✓ or ✗ — all without creating a single custom type!
First we define a custom marshaler for bool values:
Then we define a custom marshaler for bool-like strings:
Finally, we combine marshalers with JoinMarshalers and pass them to the marshaling function using the WithMarshalers option:
Isn't that cool?
There are plenty of other goodies, like support for I/O readers and writers, nested objects inlining, a plethora of options, and a huge performance boost. So, again, I encourage you to check out the post dedicated to v2 changes.
# Container-aware GOMAXPROCS
The GOMAXPROCS runtime setting controls the maximum number of operating system threads the Go scheduler can use to execute goroutines concurrently. Since Go 1.5, it defaults to the value of runtime.NumCPU, which is the number of logical CPUs on the machine (strictly speaking, this is either the total number of logical CPUs or the number allowed by the CPU affinity mask, whichever is lower).
For example, on my 8-core laptop, the default value of GOMAXPROCS is also 8:
Go programs often run in containers, like those managed by Docker or Kubernetes. These systems let you limit the CPU resources for a container using a Linux feature called cgroups.
A cgroup (control group) in Linux lets you group processes together and control how much CPU, memory, and network I/O they can use by setting limits and priorities.
For example, here's how you can limit a Docker container to use only four CPUs:
Before version 1.25, the Go runtime didn't consider the CPU quota when setting the GOMAXPROCS value. No matter how you limited CPU resources, GOMAXPROCS was always set to the number of logical CPUs on the host machine:
Now, the Go runtime started to respect the CPU quota:
The default value of GOMAXPROCS is now set to either the number of logical CPUs or the CPU limit enforced by cgroup settings for the process, whichever is lower.
Fractional CPU limits are rounded up:
On a machine with multiple CPUs, the minimum default value for GOMAXPROCS is 2, even if the CPU limit is set lower:
The Go runtime automatically updates GOMAXPROCS if the CPU limit changes. The current implementation updates up to once per second (less if the application is idle).
Note on CPU limits
Cgroups actually offer not just one, but two ways to limit CPU resources:
- CPU quota — the maximum CPU time the cgroup may use within some period window.
- CPU shares — relative CPU priorities given to the kernel scheduler.
Docker's --cpus and --cpu-period/--cpu-quota set the quota, while --cpu-shares sets the shares.
Kubernetes' CPU limit sets the quota, while CPU request sets the shares.
Go's runtime GOMAXPROCS only takes the CPU quota into account, not the shares.
You can set GOMAXPROCS manually using the runtime.GOMAXPROCS function. In this case, the runtime will use the value you provide and won't try to change it:
You can also undo any manual changes you made — whether by setting the GOMAXPROCS environment variable or calling runtime.GOMAXPROCS() — and return to the default value chosen by the runtime. To do this, use the new runtime.SetDefaultGOMAXPROCS function:
To provide backward compatibility, the new GOMAXPROCS behavior only takes effect if your program uses Go version 1.25 or higher in your go.mod. You can also turn if off manually using these GODEBUG settings:
- containermaxprocs=0 to ignore the cgroups CPU quota.
- updatemaxprocs=0 to prevent GOMAXPROCS from updating automatically.
𝗣 73193 • 𝗖𝗟 668638, 670497, 672277, 677037
# Green Tea garbage collector
Some of us Go folks used to joke about Java and its many garbage collectors, each worse than the other. Now the joke's on us — there's a new experimental garbage collector coming to Go.
Green Tea is a garbage collecting algorithm optimized for programs that create lots of small objects and run on modern computers with many CPU cores.
The current GC scans memory in a way that jumps around a lot, which is slow because it causes many delays waiting for memory. The problem gets even worse on high-performance systems with many cores and non-uniform memory architectures, where each CPU or group of CPUs has its own "local" RAM.
Green Tea takes a different approach. Instead of scanning individual small objects, it scans memory in much larger, contiguous blocks — spans. Each span contains many small objects of the same size. By working with bigger blocks, the GC can scan more efficiently and make better use of the CPU's memory cache.
Benchmark results vary, but the Go team expects a 10–40% reduction in garbage collection overhead in real-world programs that heavily use the GC.
I ran an informal test by doing 1,000,000 reads and writes to Redka (my Redis clone written in Go), and observed similar GC pause times both the old and new GC algorithms. But Redka probably isn't the best example here because it relies heavily on SQLite, and the Go part is pretty minimal.
The new garbage collector is experimental and can be enabled by setting GOEXPERIMENT=greenteagc at build time. The design and implementation of the GC may change in future releases. For more information or to give feedback, see the proposal.
# CSRF protection
The new http.CrossOriginProtection type implements protection against cross-site request forgery (CSRF) by rejecting non-safe cross-origin browser requests.
It detects cross-origin requests in these ways:
- By checking the Sec-Fetch-Site header, if it's present.
- By comparing the hostname in the Origin header to the one in the Host header.
Here's an example where we enable CrossOriginProtection and explicitly allow some extra origins:
Now, if the browser sends a request from the same domain the server is using, the server will allow it:
If the browser sends a cross-origin request, the server will reject it:
If the Origin header doesn't match the Host header, the server will reject the request:
If the request comes from a trusted origin, the server will allow it:
The server will always allow GET, HEAD, and OPTIONS methods because they are safe:
The server will always allow requests without Sec-Fetch-Site or Origin headers (by design):
# Go wait group, go!
Everyone knows how to run a goroutine with a wait group:
The new WaitGroup.Go method automatically increments the wait group counter, runs a function in a goroutine, and decrements the counter when it's done. This means we can rewrite the example above without using wg.Add() and wg.Done():
The implementation is just what you'd expect:
It's curious that it took the Go team 13 years to add a simple "Add+Done" wrapper. But hey, better late than never!
# Flight recording
Flight recording is a tracing technique that collects execution data, such as function calls and memory allocations, within a sliding window that's limited by size or duration. It helps to record traces of interesting program behavior, even if you don't know in advance when it will happen.
The new trace.FlightRecorder type implements a flight recorder in Go. It tracks a moving window over the execution trace produced by the runtime, always containing the most recent trace data.
Here's an example of how you might use it.
First, configure the sliding window:
Then create the recorder and start it:
Continue with the application code as usual:
Finally, save the trace snapshot to a file when an important event occurs:
Use the go tool to view the trace in the browser:
# More Root methods
The os.Root type, which limits filesystem operations to a specific directory, now supports several new methods similar to functions already found in the os package.
Chmod changes the mode of a file:
Chown changes the numeric user ID (uid) and group ID (gid) of a file:
Chtimes changes the access and modification times of a file:
Link creates a hard link to a file:
MkdirAll creates a new directory and any parent directories that don't already exist:
RemoveAll removes a file or a directory and any children that it contains:
Rename renames (moves) a file or a directory:
Symlink creates a symbolic link to a file. Readlink returns the destination of the symbolic link:
WriteFile writes data to a file, creating it if necessary. ReadFile reads the file and returns its contents:
Since there are now plenty of os.Root methods, you probably don't need the file-related os functions most of the time. This can make working with files much safer.
Speaking of file systems, the ones returned by os.DirFS() (a file system rooted at the given directory) and os.Root.FS() (a file system for the tree of files in the root) both implement the new fs.ReadLinkFS interface. It has two methods — ReadLink and Lstat.
ReadLink returns the destination of the symbolic link:
I have to say, the inconsistent naming between os.Root.Readlink and fs.ReadLinkFS.ReadLink is quite surprising. Maybe it's not too late to fix?
Lstat returns the information about a file or a symbolic link:
That's it for the os package!
𝗣 49580, 67002, 73126 • 𝗖𝗟 645718, 648295, 649515, 649536, 658995, 659416, 659757, 660635, 661595, 674116, 674315, 676135
# Reflective type assertion
To convert a reflect.Value back to a specific type, you typically use the Value.Interface() method combined with a type assertion:
Now you can use the new generic reflect.TypeAssert function instead:
It's more idiomatic and avoids unnecessary memory allocations, since the value is never boxed in an interface.
# Test attributes and friends
With the new T.Attr method, you can add extra test information, like a link to an issue, a test description, or anything else you need to analyze the test results:
Attributes can be especially useful in JSON format if you send the test output to a CI or other system for automatic processing:
The output is formatted to make it easier to read.
The same Attr method is also available on testing.B and testing.F.
The new T.Output method lets you access the output stream (io.Writer) used by the test. This can be helpful if you want to send your application log to the test log stream, making it easier to read or analyze automatically:
The same Output method is also available on testing.B and testing.F.
Last but not least, the testing.AllocsPerRun function will now panic if parallel tests are running.
Compare 1.24 behavior:
To 1.25:
The thing is, the result of AllocsPerRun is inherently flaky if other tests are running in parallel. That's why there's a new panicking behavior — it should help catch these kinds of bugs.
# Grouped attributes for logging
With structured logging, you often group related attributes under a single key:
It works just fine — unless you want to gather the attributes first:
slog.Group expects a slice of any values, so it doesn't accept a slice of slog.Attrs.
The new slog.GroupAttrs function fixes this issue by creating a group from the given slog.Attrs:
Not a big deal, but can be quite handy.
# Hash cloner
The new hash.Cloner interface defines a hash function that can return a copy of its current state:
All standard library hash.Hash implementations now provide the Clone function, including MD5, SHA-1, SHA-3, FNV-1, CRC-64, and others:
# Final thoughts
Go 1.25 finalizes support for testing concurrent code, introduces a major experimental JSON package, and improves the runtime with a new GOMAXPROCS design and garbage collector. It also adds a flight recorder, modern CSRF protection, a long-awaited wait group shortcut, and several other improvements.
All in all, a great release!
P.S. To catch up on other Go releases, check out the Go features by version list or explore the interactive tours for Go 1.24 and 1.23.
P.P.S. Want to learn more about Go? Check out my interactive book on concurrency
★ Subscribe to keep up with new posts.
.png)

