Go 1.24 interactive tour Go 1.24 交互式教程
Go 1.24 is scheduled for release in February, 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.
Go 1.24 计划在 2 月份发布,因此现在是探索新内容的好时机。官方发布 说明非常枯燥,因此我准备了一个交互式版本,其中包含许多示例,展示了已更改的内容和新行为。
Read on and see! 继续阅读,看看吧!
Generic aliases •
Weak pointers •
Improved finalizers •
Swiss tables •
Concurrent map •
Directory scope •
Benchmark loop •
Synthetic time •
Test context •
Discard logs •
Appender interfaces •
More iterators •
SHA-3 and friends •
HTTP protocols •
Omit zeros •
Random text •
Tool dependencies •
JSON output •
Main version •
Summary
泛型 别名 •
弱 指针 •
改进的 终结器 •
瑞士 餐桌 •
并发 地图 •
目录 范围 •
基准测试 循环 •
合成 时间 •
测试 上下文 •
丢弃 日志 •
附加程序 接口 •
更多 迭代器 •
SHA-3 和 朋友们 •
HTTP 协议 •
省略 零 •
随机 文本 •
工具 依赖关系 •
JSON 输出 •
主要 版本 •
总结
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.
本文基于 The Go Authors 的官方发行说明,根据 BSD-3-Clause 许可证获得许可。这不是一个详尽的列表;请参阅官方发行说明。
I also provide links to the proposals (𝗣) and commits (𝗖𝗟) for the features described. Check them out for motivation and implementation details.
我还提供了指向所描述功能的提案 (P) 和提交 (CL) 的链接。查看它们以了解动机和实现细节。
Generic type aliases 泛型类型别名
A quick refresher: type alias in Go creates a synonym for a type without creating a new type.
快速复习一下:Go 中的 type alias 为类型创建一个同义词,而不创建新类型。
When a type is defined based on another type, the types are different:
当一个类型基于另一个类型定义时,类型是不同的:
type ID int
var n int = 10
var id ID = 10
// id = n
// Compile-time error:
// cannot use n (variable of type int) as ID value in assignment
id = ID(n)
fmt.Printf("id is %T\n", id)
When a type is declared as an alias of another type, the types remain the same:
当一个类型被声明为另一个类型的别名时,类型保持不变:
type ID = int
var n int = 10
var id ID = 10
id = n // works fine
fmt.Printf("id is %T\n", id)
Go 1.24 supports generic type aliases: a type alias can be parameterized like a defined type. For example, you can define Set
as a generic alias to a map
with boolean values (not that it helps much):
Go 1.24 支持泛型类型别名:类型别名可以像定义的类型一样参数化。例如,你可以将 Set
定义为具有布尔值的 map
的泛型别名(不是说它有多大帮助):
type Set[T comparable] = map[T]bool
set := Set[string]{"one": true, "two": true}
fmt.Println("'one' in set:", set["one"])
fmt.Println("'six' in set:", set["six"])
fmt.Printf("set is %T\n", set)
The language spec is updated accordingly. For now, you can disable the feature by setting GOEXPERIMENT=noaliastypeparams
, but this option will be removed in Go 1.25.
语言规范也相应地更新了。目前,你可以通过设置 GOEXPERIMENT=noaliastypeparams
来禁用该功能,但此选项将在 Go 1.25 中删除。
Weak pointers 弱指针
A weak
pointer references an object like a regular pointer. But unlike a regular pointer, a weak pointer cannot keep an object alive. If only weak pointers reference an object, the garbage collector can reclaim its memory.弱
指针引用对象的方式与常规指针类似。但与常规指针不同的是,弱指针无法使对象保持活动状态。如果只有弱指针引用对象,则垃圾回收器可以回收其内存。
Let's say we have a blob type:
假设我们有一个 blob 类型:
// Blob is a large byte slice.
type Blob []byte
func (b Blob) String() string {
return fmt.Sprintf("Blob(%d KB)", len(b)/1024)
}
// newBlob returns a new Blob of the given size in KB.
func newBlob(size int) *Blob {
b := make([]byte, size*1024)
for i := range size {
b[i] = byte(i) % 255
}
return (*Blob)(&b)
}
And a pointer to a 1024 KB blob:
以及指向 1024 KB blob 的指针:
func main() {
b := newBlob(1000) // 1000 KB
fmt.Println(b)
}
We can create a weak pointer (weak.Pointer
) from a regular one with weak.Make
, and access the original pointer using the Pointer.Value
method:
我们可以从具有 weak 的常规指针 (weak.Pointer
) 创建一个弱指针 (weak.Make
) 并使用 Pointer.Value
方法访问原始指针:
func main() {
wb := weak.Make(newBlob(1000)) // 1000 KB
fmt.Println(wb.Value())
}
The regular pointer prevents the garbage collector from reclaiming the memory occupied by an object:
常规指针可防止垃圾回收器回收对象占用的内存:
func main() {
heapSize := getAlloc()
b := newBlob(1000)
fmt.Println("value before GC =", b)
runtime.GC()
fmt.Println("value after GC =", b)
fmt.Printf("heap size delta = %d KB\n", heapDelta(heapSize))
}
What are getAlloc and heapDelta
什么是 getAlloc 和 heapDelta
// heapDelta returns the delta in KB between
// the current heap size and the previous heap size.
func heapDelta(prev uint64) uint64 {
cur := getAlloc()
if cur < prev {
return 0
}
return cur - prev
}
// getAlloc returns the current heap size in KB.
func getAlloc() uint64 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.Alloc / 1024
}
The weak pointer allows the garbage collector to free the memory:
弱指针允许垃圾回收器释放内存:
func main() {
heapSize := getAlloc()
wb := weak.Make(newBlob(1000))
fmt.Println("value before GC =", wb.Value())
runtime.GC()
fmt.Println("value after GC =", wb.Value())
fmt.Printf("heap size delta = %d KB\n", heapDelta(heapSize))
}
As you can see, Pointer.Value
returns nil if the original pointer's value was reclaimed by the garbage collector. Note that it's not guaranteed to return nil as soon as an object is no longer referenced (or at any time later); the runtime decides when to reclaim the memory.
如你所见, Pointer.Value
如果原始指针的值被垃圾回收器回收,则返回 nil。请注意,不能保证在不再引用对象时 (或以后的任何时间) 返回 nil;运行时决定何时回收内存。
Weak pointers are useful for implementing a cache of large objects, ensuring that an object isn't kept alive just because it's in the cache. See the next section for an example.
弱指针对于实现大型对象的缓存非常有用,确保对象不会仅仅因为它在缓存中而保持活动状态。有关示例,请参阅下一节。
𝗣 67552 •
𝗖𝗟 628455
P 67552 • CL 628455
Improved finalizers 改进的终结器
Remember our blob? 还记得我们的 blob 吗?
func main() {
b := newBlob(1000)
fmt.Printf("b=%v, type=%T\n", b, b)
}
What if we want to run a cleanup function when the blob is garbage collected?
如果我们想在 blob 被垃圾回收时运行清理函数,该怎么办?
Previously, we'd call runtime.SetFinalizer
, which is notoriously hard to use. Now there's a better solution with runtime.AddCleanup
:
以前,我们会调用 runtime.SetFinalizer
,这是出了名的难用。现在有了更好的 runtime 解决方案。添加清理
:
func main() {
b := newBlob(1000)
now := time.Now()
// Register a cleanup function to run
// when the object is no longer reachable.
runtime.AddCleanup(b, cleanup, now)
time.Sleep(10 * time.Millisecond)
b = nil
runtime.GC()
time.Sleep(10 * time.Millisecond)
}
func cleanup(created time.Time) {
fmt.Printf(
"object is cleaned up! lifetime = %dms\n",
time.Since(created)/time.Millisecond,
)
}
AddCleanup
attaches a cleanup function to an object that runs when the object is no longer reachable. The cleanup function runs in a separate goroutine, which handles all cleanup calls for a program sequentially. Multiple cleanups can be attached to the same pointer.AddCleanup
将清理函数附加到对象上,该函数在对象不再可访问时运行。cleanup 函数在单独的 goroutine 中运行,该 goroutine 按顺序处理程序的所有清理调用。多个清理可以附加到同一个指针。
Note an argument to the cleanup function:
请注意 cleanup 函数的参数:
// AddCleanup attaches a cleanup function to ptr.
// Some time after ptr is no longer reachable,
// the runtime will call cleanup(arg) in a separate goroutine.
func AddCleanup[T, S any](ptr *T, cleanup func(S), arg S) Cleanup
In the example above, we passed the creation time as an argument, but typically it would be a resource we want to clean up when the pointer is garbage collected.
在上面的示例中,我们将创建时间作为参数传递,但通常它是我们希望在指针被垃圾回收时清理的资源。
Here's an example. Suppose we want to implement a WeakMap
, where an item can be discarded if no one references it's value. Let's use a map
with weak.Pointer
values:
这是一个例子。假设我们想要实现一个 WeakMap
,如果没有人引用它的值,则可以丢弃一个项目。让我们使用一个具有 weak.指针
值的 Map
:
// WeakMap is a map with weakly referenced values.
type WeakMap[K comparable, V any] struct {
store map[K]weak.Pointer[V]
mu sync.Mutex
}
// NewWeakMap creates a new WeakMap.
func NewWeakMap[K comparable, V any]() *WeakMap[K, V] {
return &WeakMap[K, V]{
store: make(map[K]weak.Pointer[V]),
}
}
// Len returns the number of items in the map.
func (wm *WeakMap[K, V]) Len() int {
wm.mu.Lock()
defer wm.mu.Unlock()
return len(wm.store)
}
Getting a value is straightforward:
获取值非常简单:
// Get returns the value stored in the map for a key,
// or nil if no value is present.
func (wm *WeakMap[K, V]) Get(key K) *V {
wm.mu.Lock()
defer wm.mu.Unlock()
if wp, found := wm.store[key]; found {
return wp.Value()
}
return nil
}
Now, how do we ensure the item is removed from the map when the runtime reclaims the value? With runtime.AddCleanup
, it's simple:
现在,我们如何确保在运行时回收值时从地图中删除该项目?使用 runtime.AddCleanup
,这很简单:
// Set sets the value for a key.
func (wm *WeakMap[K, V]) Set(key K, value *V) {
wm.mu.Lock()
defer wm.mu.Unlock()
// Create a weak pointer for the value.
wp := weak.Make(value)
// Remove the item when the value is reclaimed.
runtime.AddCleanup(value, wm.Delete, key)
// Store the weak pointer in the map.
wm.store[key] = wp
}
// Delete removes an item for a key.
func (wm *WeakMap[K, V]) Delete(key K) {
wm.mu.Lock()
defer wm.mu.Unlock()
delete(wm.store, key)
}
We pass the current key to the cleanup function (wm.Delete
) so it knows which item to remove from the map.
我们将当前键传递给清理函数 (wm.Delete
),以便它知道要从 map 中删除哪个项目。
var sink *Blob
func main() {
wm := NewWeakMap[string, Blob]()
wm.Set("one", newBlob(10))
wm.Set("two", newBlob(20))
fmt.Println("Before GC:")
fmt.Println("len(map) =", wm.Len())
fmt.Println("map[one] =", wm.Get("one"))
fmt.Println("map[two] =", wm.Get("two"))
// Allow the garbage collector to reclaim
// the second item, but not the first one.
sink = wm.Get("one")
runtime.GC()
fmt.Println("After GC:")
fmt.Println("len(map) =", wm.Len())
fmt.Println("map[one] =", wm.Get("one"))
fmt.Println("map[two] =", wm.Get("two"))
}
Works like a charm! 像魅力一样工作!
Note that the cleanup function is not guaranteed to run immediately after an object is no longer referenced; it may execute at an arbitrary time in the future.
请注意,cleanup 函数不能保证在不再引用对象后立即运行;它可能会在将来的任意时间执行。
With the introduction of AddCleanup
, the usage of SetFinalizer
is discouraged. New code should prefer AddCleanup
.
引入 AddCleanup
后,不建议使用 SetFinalizer
。新代码应首选 AddCleanup
。
𝗣 67535 •
𝗖𝗟 627695, 627975
P 67535 • CL 627695, 627975
Swiss tables 瑞士式餐桌
After many years, the Go team decided to change the underlying map
implementation! It is now based on SwissTable, which offers several optimizations:
- Access and assignment of large (>1024 entries) maps improved ~30%.
- Assignment into pre-sized maps improved ~35%.
- Iteration faster across the board by ~10%, ~60% for maps with low load (large size, few entries).
Benchmarks
These results are missing a few optimizations, but give a good overview of changes.
│ /tmp/noswiss.lu.txt │ /tmp/swiss.lu.txt │
│ sec/op │ sec/op vs base │
MapIter/impl=runtimeMap/t=Int64/len=64-12 642.0n ± 3% 603.8n ± 6% -5.95% (p=0.004 n=6)
MapIter/impl=runtimeMap/t=Int64/len=8192-12 87.98µ ± 1% 78.80µ ± 1% -10.43% (p=0.002 n=6)
MapIter/impl=runtimeMap/t=Int64/len=4194304-12 47.40m ± 2% 44.41m ± 2% -6.30% (p=0.002 n=6)
MapIterLowLoad/impl=runtimeMap/t=Int64/len=64-12 145.85n ± 3% 92.85n ± 2% -36.34% (p=0.002 n=6)
MapIterLowLoad/impl=runtimeMap/t=Int64/len=8192-12 13.205µ ± 0% 6.078µ ± 1% -53.97% (p=0.002 n=6)
MapIterLowLoad/impl=runtimeMap/t=Int64/len=4194304-12 15.20m ± 1% 18.22m ± 1% +19.87% (p=0.002 n=6)
MapIterGrow/impl=runtimeMap/t=Int64/len=64-12 10.196µ ± 2% 8.092µ ± 8% -20.63% (p=0.002 n=6)
MapIterGrow/impl=runtimeMap/t=Int64/len=8192-12 1.259m ± 2% 1.008m ± 4% -19.97% (p=0.002 n=6)
MapIterGrow/impl=runtimeMap/t=Int64/len=4194304-12 1.424 ± 5% 1.275 ± 0% -10.47% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int64/len=64-12 14.08n ± 4% 15.28n ± 3% +8.45% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int64/len=8192-12 27.61n ± 1% 18.80n ± 1% -31.89% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int64/len=4194304-12 82.94n ± 1% 102.20n ± 0% +23.22% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int32/len=64-12 13.84n ± 5% 15.56n ± 2% +12.39% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int32/len=8192-12 26.90n ± 2% 18.47n ± 2% -31.34% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=Int32/len=4194304-12 79.60n ± 0% 93.00n ± 0% +16.83% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=String/len=64-12 16.36n ± 6% 18.69n ± 1% +14.24% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=String/len=8192-12 38.39n ± 1% 25.67n ± 1% -33.13% (p=0.002 n=6)
MapGetHit/impl=runtimeMap/t=String/len=4194304-12 146.0n ± 1% 172.2n ± 1% +17.95% (p=0.002 n=6)
MapGetMiss/impl=runtimeMap/t=Int64/len=64-12 15.63n ± 8% 15.08n ± 8% ~ (p=0.240 n=6)
MapGetMiss/impl=runtimeMap/t=Int64/len=8192-12 17.55n ± 1% 17.59n ± 4% ~ (p=0.909 n=6)
MapGetMiss/impl=runtimeMap/t=Int64/len=4194304-12 106.40n ± 1% 72.99n ± 2% -31.40% (p=0.002 n=6)
MapGetMiss/impl=runtimeMap/t=Int32/len=64-12 15.63n ± 7% 15.27n ± 8% ~ (p=0.132 n=6)
MapGetMiss/impl=runtimeMap/t=Int32/len=8192-12 17.18n ± 3% 17.25n ± 1% ~ (p=0.729 n=6)
MapGetMiss/impl=runtimeMap/t=Int32/len=4194304-12 100.15n ± 1% 74.71n ± 1% -25.40% (p=0.002 n=6)
MapGetMiss/impl=runtimeMap/t=String/len=64-12 18.96n ± 3% 18.19n ± 11% ~ (p=0.132 n=6)
MapGetMiss/impl=runtimeMap/t=String/len=8192-12 23.79n ± 3% 20.98n ± 2% -11.79% (p=0.002 n=6)
MapGetMiss/impl=runtimeMap/t=String/len=4194304-12 134.85n ± 1% 84.82n ± 1% -37.10% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=Int64/len=64-12 5.886µ ± 3% 5.699µ ± 3% -3.18% (p=0.015 n=6)
MapPutGrow/impl=runtimeMap/t=Int64/len=8192-12 739.1µ ± 2% 816.0µ ± 4% +10.41% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=Int64/len=4194304-12 929.3m ± 1% 894.2m ± 5% ~ (p=0.065 n=6)
MapPutGrow/impl=runtimeMap/t=Int32/len=64-12 5.487µ ± 4% 5.326µ ± 2% -2.93% (p=0.028 n=6)
MapPutGrow/impl=runtimeMap/t=Int32/len=8192-12 681.6µ ± 2% 767.3µ ± 2% +12.58% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=Int32/len=4194304-12 831.9m ± 2% 802.9m ± 1% -3.49% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=String/len=64-12 7.607µ ± 2% 7.379µ ± 2% -2.99% (p=0.002 n=6)
MapPutGrow/impl=runtimeMap/t=String/len=8192-12 1.204m ± 4% 1.212m ± 4% ~ (p=0.310 n=6)
MapPutGrow/impl=runtimeMap/t=String/len=4194304-12 1.699 ± 2% 1.876 ± 1% +10.37% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int64/len=64-12 2.179µ ± 1% 1.428µ ± 5% -34.47% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int64/len=8192-12 277.6µ ± 2% 198.6µ ± 1% -28.45% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int64/len=4194304-12 389.7m ± 1% 518.2m ± 1% +32.97% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int32/len=64-12 1.784µ ± 2% 1.110µ ± 3% -37.78% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int32/len=8192-12 228.1µ ± 5% 151.4µ ± 4% -33.62% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=Int32/len=4194304-12 361.5m ± 1% 481.2m ± 1% +33.10% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=String/len=64-12 2.670µ ± 3% 2.167µ ± 3% -18.81% (p=0.002 n=6)
MapPutPreAllocate/impl=runtimeMap/t=String/len=8192-12 380.1µ ± 2% 417.2µ ± 9% +9.77% (p=0.015 n=6)
MapPutPreAllocate/impl=runtimeMap/t=String/len=4194304-12 493.1m ± 4% 718.1m ± 7% +45.62% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=Int32/len=64-12 1421.0n ± 3% 804.0n ± 5% -43.42% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=Int32/len=8192-12 192.4µ ± 1% 120.6µ ± 1% -37.30% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=Int32/len=4194304-12 364.0m ± 2% 473.0m ± 2% +29.95% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=String/len=64-12 1.602µ ± 4% 1.083µ ± 14% -32.41% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=String/len=8192-12 232.4µ ± 1% 165.7µ ± 2% -28.68% (p=0.002 n=6)
MapPutReuse/impl=runtimeMap/t=String/len=4194304-12 440.4m ± 2% 672.5m ± 1% +52.72% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int64/len=64-12 34.25n ± 3% 37.76n ± 5% +10.23% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int64/len=8192-12 57.91n ± 2% 45.24n ± 2% -21.89% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int64/len=4194304-12 170.5n ± 0% 222.0n ± 1% +30.20% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int32/len=64-12 34.06n ± 4% 37.87n ± 6% +11.16% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int32/len=8192-12 54.92n ± 1% 43.41n ± 2% -20.96% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=Int32/len=4194304-12 153.4n ± 1% 178.3n ± 2% +16.26% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=String/len=64-12 42.11n ± 8% 48.48n ± 7% +15.12% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=String/len=8192-12 78.46n ± 1% 56.10n ± 2% -28.50% (p=0.002 n=6)
MapPutDelete/impl=runtimeMap/t=String/len=4194304-12 204.6n ± 1% 261.4n ± 1% +27.76% (p=0.002 n=6)
You can disable the new implementation by setting GOEXPERIMENT=noswissmap
at build time.
𝗣 54766
Concurrent hash-trie map
The implementation of sync.Map
has changed to a concurrent hash-trie, improving performance, especially for map modifications. Modifications of disjoint key sets are less likely to contend on larger maps, and no ramp-up time is needed to achieve low-contention loads.
The new implementation outperforms the old one on nearly every benchmark:
│ before │ after │
│ sec/op │ sec/op vs base │
MapLoadMostlyHits 7.870n ± 1% 8.415n ± 3% +6.93% (p=0.002 n=6)
MapLoadMostlyMisses 7.210n ± 1% 5.314n ± 2% -26.28% (p=0.002 n=6)
MapLoadOrStoreBalanced 360.10n ± 18% 71.78n ± 2% -80.07% (p=0.002 n=6)
MapLoadOrStoreUnique 707.2n ± 18% 135.2n ± 4% -80.88% (p=0.002 n=6)
MapLoadOrStoreCollision 5.089n ± 201% 3.963n ± 1% -22.11% (p=0.002 n=6)
MapLoadAndDeleteBalanced 17.045n ± 64% 5.280n ± 1% -69.02% (p=0.002 n=6)
MapLoadAndDeleteUnique 14.250n ± 57% 6.452n ± 1% ~ (p=0.368 n=6)
MapLoadAndDeleteCollision 19.34n ± 39% 23.31n ± 27% ~ (p=0.180 n=6)
MapRange 3.055µ ± 3% 1.918µ ± 2% -37.23% (p=0.002 n=6)
MapAdversarialAlloc 245.30n ± 6% 14.90n ± 23% -93.92% (p=0.002 n=6)
MapAdversarialDelete 143.550n ± 2% 8.184n ± 1% -94.30% (p=0.002 n=6)
MapDeleteCollision 9.199n ± 65% 3.165n ± 1% -65.59% (p=0.002 n=6)
MapSwapCollision 164.7n ± 7% 108.7n ± 36% -34.01% (p=0.002 n=6)
MapSwapMostlyHits 33.12n ± 15% 35.79n ± 9% ~ (p=0.180 n=6)
MapSwapMostlyMisses 604.5n ± 5% 280.2n ± 7% -53.64% (p=0.002 n=6)
MapCompareAndSwapCollision 96.02n ± 40% 69.93n ± 24% -27.17% (p=0.041 n=6)
MapCompareAndSwapNoExistingKey 6.345n ± 1% 6.202n ± 1% -2.24% (p=0.002 n=6)
MapCompareAndSwapValueNotEqual 6.121n ± 3% 5.564n ± 4% -9.09% (p=0.002 n=6)
MapCompareAndSwapMostlyHits 44.21n ± 13% 43.46n ± 11% ~ (p=0.485 n=6)
MapCompareAndSwapMostlyMisses 33.51n ± 6% 13.51n ± 5% -59.70% (p=0.002 n=6)
MapCompareAndDeleteCollision 27.85n ± 104% 31.02n ± 26% ~ (p=0.180 n=6)
MapCompareAndDeleteMostlyHits 50.43n ± 33% 109.45n ± 8% +117.03% (p=0.002 n=6)
MapCompareAndDeleteMostlyMisses 27.17n ± 7% 11.37n ± 3% -58.14% (p=0.002 n=6)
MapClear 300.2n ± 5% 124.2n ± 8% -58.64% (p=0.002 n=6)
geomean 50.38n 25.79n -48.81%
The load-hit case (MapLoadMostlyHits
) is slightly slower due to Swiss Tables improving the performance of the old sync.Map
. Some benchmarks show a seemingly large slowdown, but that's mainly due to the fact that the new implementation can actually shrink, whereas the old one never shrank. This creates additional allocations.
The concurrent hash-trie map (HashTrieMap
) was initially added for the unique
package in Go 1.23. It proved faster than the original sync.Map
in many cases, so the Go team reimplemented sync.Map
as a wrapper for HashTrieMap
.
You can disable the new implementation by setting GOEXPERIMENT=nosynchashtriemap
at build time.
Directory-scoped filesystem access
The new os.Root
type restricts filesystem operations to a specific directory.
The OpenRoot
function opens a directory and returns a Root
:
dir, err := os.OpenRoot("data")
fmt.Printf("opened root=%s, err=%v\n", dir.Name(), err)
Methods on Root
operate within the directory and do not allow paths outside the directory:
file, err := dir.Open("01.txt")
fmt.Printf("opened file=%s, err=%v\n", file.Name(), err)
file, err = dir.Open("../main.txt")
fmt.Printf("opened file=%v, err=%v\n", file, err)
Methods on Root
mirror most file system operations available in the os
package:
file, err := dir.Create("new.txt")
fmt.Printf("created file=%s, err=%v\n", file.Name(), err)
stat, err := dir.Stat("02.txt")
fmt.Printf(
"file info: name=%s, size=%dB, mode=%v, err=%v\n",
stat.Name(), stat.Size(), stat.Mode(), err,
)
err = dir.Remove("03.txt")
fmt.Printf("deleted 03.txt, err=%v\n", err)
You should close the Root
after you are done with it:
func process(dir string) error {
r, err := os.OpenRoot(dir)
if err != nil {
return err
}
defer r.Close()
// do stuff
return nil
}
After the Root
is closed, calling its methods return errors:
err = dir.Close()
fmt.Printf("closed root, err=%v\n", err)
file, err := dir.Open("01.txt")
fmt.Printf("opened file=%v, err=%v\n", file, err)
Root
methods follow symbolic links, but these links cannot reference locations outside the root. Symbolic links must be relative. Methods do not restrict traversal of filesystem boundaries, Linux bind mounts, /proc special files, or access to Unix device files.
On most platforms, creating a Root
opens a file descriptor or handle for the directory. If the directory is moved, Root
methods reference the directory in its new location.
𝗣 67002 • 𝗖𝗟 612136, 627076, 627475, 629518, 629555
Benchmark loop
You are probably familiar with a benchmark loop (for range b.N
):
var sink int
func BenchmarkSlicesMax(b *testing.B) {
// Setup the benchmark.
s := randomSlice(10_000)
b.ResetTimer()
// Run the benchmark.
for range b.N {
sink = slices.Max(s)
}
}
Go conveniently handles the mechanics of running benchmarks, determines a reasonable b.N
, and provides the final results in nanoseconds per operation.
Yet, there are a few nuances to keep in mind:
- The benchmark function (
BenchmarkSlicesMax
) runs multiple times, so the setup step also runs multiple times (nothing we can do about that). - We need to reset the benchmark timer to exclude the setup time from the benchmark time.
- We have to ensure the compiler doesn't optimize away the benchmarked call (
slices.Max
) by using a sink variable.
Go 1.24 introduces the faster and less error-prone testing.B.Loop
to replace the traditional for range b.N
loop:
func BenchmarkSlicesMax(b *testing.B) {
// Setup the benchmark.
s := randomSlice(10_000)
// Run the benchmark.
for b.Loop() {
slices.Max(s)
}
}
b.Loop
solves issues with the b.N
method:
- The benchmark function executes once per
-count
, so setup and cleanup steps run only once. - Everything outside the
b.Loop
doesn't affect the benchmark time, sob.ResetTimer
isn't needed. - Compiler never optimizes away calls to functions within the body of a
b.Loop
.
Benchmarks should use either b.Loop
or a b.N
-style loop, but not both.
𝗣 61515 • 𝗖𝗟 608798, 612043, 612835, 627755, 635898
Synthetic time for testing
Suppose we have a function that waits for a value from a channel for one minute, then times out:
// Read reads a value from the input channel and returns it.
// Timeouts after 60 seconds.
func Read(in chan int) (int, error) {
select {
case v := <-in:
return v, nil
case <-time.After(60 * time.Second):
return 0, errors.New("timeout")
}
}
We use it like this:
func main() {
ch := make(chan int)
go func() {
ch <- 42
}()
val, err := Read(ch)
fmt.Printf("val=%v, err=%v\n", val, err)
}
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 testing/synctest
package to the rescue! The synctest.Run()
function executes an isolated "bubble" in a new goroutine. Within the bubble, time
package functions use a fake clock, allowing our test to pass instantly:
func TestReadTimeout(t *testing.T) {
synctest.Run(func() {
ch := make(chan int)
_, err := Read(ch)
if err == nil {
t.Fatal("expected timeout error, got nil")
}
})
}
PASS
Goroutines in the bubble use a synthetic time implementation (the initial time 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.
Another useful function is synctest.Wait
. It waits for all goroutines in the current bubble to block, then resumes execution:
synctest.Run(func() {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Wait just less than the timeout.
time.Sleep(timeout - time.Nanosecond)
synctest.Wait()
fmt.Printf("before timeout: ctx.Err() = %v\n", ctx.Err())
// Wait the rest of the way until the timeout.
time.Sleep(time.Nanosecond)
synctest.Wait()
fmt.Printf("after timeout: ctx.Err() = %v\n", ctx.Err())
})
before timeout: ctx.Err() = <nil>
after timeout: ctx.Err() = context deadline exceeded
The synctest
package is experimental and must be enabled by setting GOEXPERIMENT=synctest
at build time. The package API is subject to change in future releases. See the proposal for more information and to provide feeback.
Test context and working directory
Suppose we want to test this very useful server:
// Server provides answers to all questions.
type Server struct{}
// Get returns an answer from the server.
func (s *Server) Get(query string) int {
return 42
}
// startServer starts a server that can
// be stopped by canceling the context.
func startServer(ctx context.Context) *Server {
go func() {
select {
case <-ctx.Done():
// Free resources.
}
}()
return &Server{}
}
Here's a beautiful test I wrote:
func Test(t *testing.T) {
srv := startServer(context.Background())
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
Hooray, the test passed! However, there's a problem: I used an empty context, so the server didn't actually stop. Such resource leakage can be an issue, especially with many tests.
I can fix it by creating a cancelable context and canceling it when the test completes:
func Test(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
srv := startServer(ctx)
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
Even better, now I can use the new T.Context
method. It returns a context that is canceled after the test completes:
func Test(t *testing.T) {
srv := startServer(t.Context())
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
One caveat remains. What if cleaning up server resources takes time? The startServer
goroutine will initiate the cleanup when the test context closes, but will it finish before the main goroutine exits? Not necessarily.
There is a useful test context property that can help us. The test context is canceled just before T.Cleanup
-registered functions are called. So we can use T.Cleanup
to register a function that waits for the server to stop:
func Test(t *testing.T) {
srv := startServer(t.Context())
t.Cleanup(func() {
<-srv.Done()
})
if srv.Get("how much?") != 42 {
t.Fatal("unexpected value")
}
}
The server code has also changed
// Server provides answers to all questions.
type Server struct {
done chan struct{}
}
// Get returns an anser from the server.
func (s *Server) Get(query string) int {
return 42
}
// Stop stops the server.
func (s *Server) Stop() {
// Simulate a long operation.
time.Sleep(10 * time.Millisecond)
fmt.Println("server stopped")
close(s.done)
}
// Done returns a channel that's closed when the server stops.
func (s *Server) Done() <-chan struct{} {
return s.done
}
// startServer starts a server that can
// be stopped by canceling the context.
func startServer(ctx context.Context) *Server {
srv := &Server{done: make(chan struct{})}
go func() {
select {
case <-ctx.Done():
srv.Stop()
}
}()
return srv
}
Like tests, benchmarks have their own B.Context
.
Oh, and speaking of tests, the new T.Chdir
and B.Chdir
methods change the working directory for the duration of a test or benchmark:
func Test(t *testing.T) {
t.Run("test1", func(t *testing.T) {
// Change the working directory for the current test.
t.Chdir("/tmp")
cwd, _ := os.Getwd()
if cwd != "/tmp" {
t.Fatalf("unexpected cwd: %s", cwd)
}
})
t.Run("test2", func(t *testing.T) {
// This test uses the original working directory.
cwd, _ := os.Getwd()
if cwd == "/tmp" {
t.Fatalf("unexpected cwd: %s", cwd)
}
})
}
Chdir
methods use Cleanup
to restore the working directory to its original value after the test or benchmark.
Discard log output
An easy way to create a silent logger (e.g. for testing or benchmarking) is to use slog.TextHandler
with io.Discard
:
log := slog.New(
slog.NewTextHandler(io.Discard, nil),
)
log.Info("Prints nothing")
Now there's an even easier way with the slog.DiscardHandler
package-level variable:
log := slog.New(slog.DiscardHandler)
log.Info("Prints nothing")
Appender interfaces
Two new interfaces, encoding.TextAppender
and encoding.BinaryAppender
, allow appending an object's textual or binary representation to a byte slice.
type TextAppender interface {
// AppendText appends the textual representation of itself to the end of b
// (allocating a larger slice if necessary) and returns the updated slice.
//
// Implementations must not retain b, nor mutate any bytes within b[:len(b)].
AppendText(b []byte) ([]byte, error)
}
type BinaryAppender interface {
// AppendBinary appends the binary representation of itself to the end of b
// (allocating a larger slice if necessary) and returns the updated slice.
//
// Implementations must not retain b, nor mutate any bytes within b[:len(b)].
AppendBinary(b []byte) ([]byte, error)
}
These interfaces provide the same functionality as TextMarshaler
and BinaryMarshaler
, but instead of allocating a new slice each time, they append the data directly to an existing slice.
These interfaces are now implemented by standard library types that already implemented TextMarshaler
or BinaryMarshaler
: math/big.Float
, net.IP
, regexp.Regexp
, time.Time
, and others:
// 2021-02-03T04:05:06Z
t := time.Date(2021, 2, 3, 4, 5, 6, 0, time.UTC)
var b []byte
b, err := t.AppendText(b)
fmt.Printf("b=%s, err=%v", b, err)
𝗣 62384 • 𝗖𝗟 601595, 601776, 603255, 603815, 605056, 605758, 606655, 607079, 607520, 634515
More string and byte iterators
Go 1.23 went all in on iterators, so we see more and more of them in the standard library.
New functions in the strings
package:
Lines
returns an iterator over the newline-terminated lines in the string s
:
s := "one\ntwo\nsix"
for line := range strings.Lines(s) {
fmt.Print(line)
}
SplitSeq
returns an iterator over all substrings of s
separated by sep
:
s := "one-two-six"
for part := range strings.SplitSeq(s, "-") {
fmt.Println(part)
}
SplitAfterSeq
returns an iterator over substrings of s
split after each instance of sep
:
s := "one-two-six"
for part := range strings.SplitAfterSeq(s, "-") {
fmt.Println(part)
}
FieldsSeq
returns an iterator over substrings of s
split around runs of whitespace characters, as defined by unicode.IsSpace
:
s := "one two\nsix"
for part := range strings.FieldsSeq(s) {
fmt.Println(part)
}
FieldsFuncSeq
returns an iterator over substrings of s
split around runs of Unicode code points satisfying f(c)
:
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
s := "one,two;six..."
for part := range strings.FieldsFuncSeq(s, f) {
fmt.Println(part)
}
The same iterator functions were added to the bytes
package.
SHA-3 and friends
The new crypto/sha3
package implements the SHA-3 hash function and SHAKE and cSHAKE extendable-output functions, as defined in FIPS 202:
s := []byte("go is awesome")
fmt.Printf("Source: %s\n", s)
fmt.Printf("SHA3-224: %x\n", sha3.Sum224(s))
fmt.Printf("SHA3-256: %x\n", sha3.Sum256(s))
fmt.Printf("SHA3-384: %x\n", sha3.Sum384(s))
fmt.Printf("SHA3-512: %x\n", sha3.Sum512(s))
And two more crypto
packages:
crypto/hkdf
package implements the HMAC-based Extract-and-Expand key derivation function HKDF, as defined in RFC 5869.
crypto/pbkdf2
package implements the password-based key derivation function PBKDF2, as defined in RFC 8018.
HTTP protocols
The new Server.Protocols
and Transport.Protocols
fields in the net/http
package provide a simple way to configure what HTTP protocols a server or client use:
t := http.DefaultTransport.(*http.Transport).Clone()
// Use either HTTP/1 or HTTP/2.
t.Protocols = new(http.Protocols)
t.Protocols.SetHTTP1(true)
t.Protocols.SetHTTP2(true)
cli := &http.Client{Transport: t}
res, err := cli.Get("http://httpbingo.org/status/200")
if err != nil {
panic(err)
}
res.Body.Close()
The supported protocols are:
- HTTP1 is the HTTP/1.0 and HTTP/1.1 protocols. HTTP1 is supported on both unsecured TCP and secured TLS connections.
- HTTP2 is the HTTP/2 protcol over a TLS connection.
- UnencryptedHTTP2 is the HTTP/2 protocol over an unsecured TCP connection.
Omit zero values in JSON
The new omitzero
option in the field tag instructs the JSON marshaler to omit zero values. It is clearer and less error-prone than omitempty
when the intent is to omit zero values. Unlike omitempty
, omitzero
omits zero-valued time.Time
values, a common source of friction.
Compare omitempty
:
type Person struct {
Name string `json:"name"`
BirthDate time.Time `json:"birth_date,omitempty"`
}
alice := Person{Name: "Alice"}
b, err := json.Marshal(alice)
fmt.Println(string(b), err)
To omitzero
:
type Person struct {
Name string `json:"name"`
BirthDate time.Time `json:"birth_date,omitzero"`
}
alice := Person{Name: "Alice"}
b, err := json.Marshal(alice)
fmt.Println(string(b), err)
If the field type has an IsZero() bool
method, it will determine whether the value is zero. Otherwise, the value is zero if it is the zero value for its type.
Random text
The crypto/rand.Text
function returns a cryptographically random string using the standard Base32 alphabet:
text := rand.Text()
fmt.Println(text)
The result contains at least 128 bits of randomness, enough to prevent brute force guessing attacks and to make the likelihood of collisions vanishingly small.
Tool dependencies
Go modules can now track executable dependencies using tool
directives in go.mod
.
To add a tool dependency, use go get -tool
:
go mod init sandbox
go get -tool golang.org/x/tools/cmd/stringer
This adds a tool
dependency with a require
directive to the go.mod
:
module sandbox
go 1.24rc1
tool golang.org/x/tools/cmd/stringer
require (
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/tools v0.29.0 // indirect
)
Tool dependencies remove the need for the previous workaround of adding tools as blank imports to a file conventionally named "tools.go". The go tool
command can now run these tools in addition to tools shipped with the Go distribution:
go tool stringer
Refer to the documentation for details.
𝗣 48429
JSON output for build, install and test
The go build
, go install
, and go test
commands now accept a -json
flag that reports build output and failures as structured JSON on standard output.
For example, here is the go test
output in default verbose mode:
go test -v
=== RUN TestSet_Add
--- PASS: TestSet_Add (0.00s)
=== RUN TestSet_Contains
--- PASS: TestSet_Contains (0.00s)
PASS
ok sandbox 0.934s
And here is the go test
output for the same program in JSON mode:
go test -json
{"Time":"2025-01-11T19:22:29.280091+05:00","Action":"start","Package":"sandbox"}
{"Time":"2025-01-11T19:22:29.671331+05:00","Action":"run","Package":"sandbox","Test":"TestSet_Add"}
{"Time":"2025-01-11T19:22:29.671418+05:00","Action":"output","Package":"sandbox","Test":"TestSet_Add","Output":"=== RUN TestSet_Add\n"}
{"Time":"2025-01-11T19:22:29.67156+05:00","Action":"output","Package":"sandbox","Test":"TestSet_Add","Output":"--- PASS: TestSet_Add (0.00s)\n"}
{"Time":"2025-01-11T19:22:29.671579+05:00","Action":"pass","Package":"sandbox","Test":"TestSet_Add","Elapsed":0}
{"Time":"2025-01-11T19:22:29.671601+05:00","Action":"run","Package":"sandbox","Test":"TestSet_Contains"}
{"Time":"2025-01-11T19:22:29.671608+05:00","Action":"output","Package":"sandbox","Test":"TestSet_Contains","Output":"=== RUN TestSet_Contains\n"}
{"Time":"2025-01-11T19:22:29.67163+05:00","Action":"output","Package":"sandbox","Test":"TestSet_Contains","Output":"--- PASS: TestSet_Contains (0.00s)\n"}
{"Time":"2025-01-11T19:22:29.671638+05:00","Action":"pass","Package":"sandbox","Test":"TestSet_Contains","Elapsed":0}
{"Time":"2025-01-11T19:22:29.671645+05:00","Action":"output","Package":"sandbox","Output":"PASS\n"}
{"Time":"2025-01-11T19:22:29.672058+05:00","Action":"output","Package":"sandbox","Output":"ok \tsandbox\t0.392s\n"}
{"Time":"2025-01-11T19:22:29.6721+05:00","Action":"pass","Package":"sandbox","Elapsed":0.392}
There may also be non-JSON error text on standard error, even with the -json
flag. Typically, this indicates an early, serious error.
For details of the JSON format, see go help buildjson
.
𝗣 62067
Main module's version
The go build
command now sets the main module's version (BuildInfo.Main.Version
) in the compiled binary based on the version control system tag or commit. A +dirty
suffix is added if there are uncommitted changes.
Here's a program that prints the version:
// get build information embedded in the running binary
info, _ := debug.ReadBuildInfo()
fmt.Println("Go version:", info.GoVersion)
fmt.Println("App version:", info.Main.Version)
Here's the output for Go 1.23:
Go version: go1.23.4
App version: (devel)
Here's the output for Go 1.24:
Go version: go1.24rc1
App version: v0.0.0-20250111143208-a7857c757b85+dirty
When the current commit matches a tagged version, the value is set to v<tag>[+dirty]
:
v1.2.4
v1.2.4+dirty
When the current commit doesn't match a tagged version, the value is set to <pseudo>[+dirty]
, where pseudo
consists of the latest tag, current date, and commit:
v1.2.3-0.20240620130020-daa7c0413123
v1.2.3-0.20240620130020-daa7c0413123+dirty
When no VCS information is available, the value is set to (devel)
(as in Go 1.23).
Use the -buildvcs=false
flag to omit version control information from the binary.
𝗣 50603
Summary
Go 1.24 introduces many new features, including weak pointers, finalizers, and directory-scoped filesystem access. A lot of effort has gone into implementing faster maps, which is a very welcome change. Also, the Go team clearly prioritizes the developer experience, offering easier and safer ways to write benchmarks, test concurrent code, and use custom tools. And of course, cryptographic improvements like SHA-3 and random text generation are a nice touch.
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.23 and 1.22.
P.P.S. Interactive examples in this post are powered by codapi — an open source tool I'm building. Use it to embed live code snippets into your product docs, online course or blog.
★ Subscribe to keep up with new posts.