这是用户在 2025-1-17 24:06 为 https://antonz.org/go-1-24/ 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

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)
Edit  编辑

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)
Edit  编辑

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)
Edit  编辑

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 中删除。

𝗣 46477  46477

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)
}
Edit  编辑

And a pointer to a 1024 KB blob:
以及指向 1024 KB blob 的指针:

func main() {
    b := newBlob(1000) // 1000 KB
    fmt.Println(b)
}
Edit  编辑

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())
}
Edit  编辑

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))
}
Edit  编辑
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))
}
Edit  编辑

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)
}
Edit  编辑

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,
    )
}
Edit  编辑

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)
}
Edit  编辑

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
}
Edit  编辑

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)
}
Edit  编辑

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"))
}
Edit  编辑

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 627695627975

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.

𝗣 70683 • 𝗖𝗟 608335

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)
Edit

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)
Edit

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)
Edit

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)
Edit

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)
    }
}
Edit

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)
    }
}
Edit

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, so b.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")
    }
}
Edit

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)
}
Edit

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.

𝗣 67434 • 𝗖𝗟 629735, 629856

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{}
}
Edit

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")
    }
}
Edit

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")
    }
}
Edit

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")
    }
}
Edit

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")
    }
}
Edit
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.

𝗣 36532 • 𝗖𝗟 603959, 637236

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)
        }
    })
}
Edit

Chdir methods use Cleanup to restore the working directory to its original value after the test or benchmark.

𝗣 62516 • 𝗖𝗟 529895

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")
Edit

Now there's an even easier way with the slog.DiscardHandler package-level variable:

log := slog.New(slog.DiscardHandler)
log.Info("Prints nothing")
Edit

𝗣 62005 • 𝗖𝗟 626486

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)
Edit

𝗣 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)
}
Edit

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)
}
Edit

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)
}
Edit

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)
}
Edit

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)
}
Edit

The same iterator functions were added to the bytes package.

𝗣 61901 • 𝗖𝗟 587095

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))
Edit

𝗣 69982 • 𝗖𝗟 629176

And two more crypto packages:

crypto/hkdf package implements the HMAC-based Extract-and-Expand key derivation function HKDF, as defined in RFC 5869.

𝗣 61477 • 𝗖𝗟 630296

crypto/pbkdf2 package implements the password-based key derivation function PBKDF2, as defined in RFC 8018.

𝗣 69488 • 𝗖𝗟 628135

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()
Edit

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.

𝗣 67814 • 𝗖𝗟 607496

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)
Edit

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)
Edit

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.

𝗣 45669 • 𝗖𝗟 615676

Random text

The crypto/rand.Text function returns a cryptographically random string using the standard Base32 alphabet:

text := rand.Text()
fmt.Println(text)
Edit

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.

𝗣 67057 • 𝗖𝗟 627477

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.