最近 Go 在前几天已经发布了 Go1.25 的 RC1 版本:

这次还是比较准时的。之前 Go1.25 新特性我们也介绍了不少,今天分享一些零碎但偶尔可能需要关注的新特性点。
Go 所要求的 OS 版本推进
每次新版本迭代,都会迎来 OS 系统版本的进一步的新老推进,是我们需要及时关注的。为此之前还推进了测试老集群的版本升级。
MacOS 系统:从 Go1.25 版本起,需要 macOS 12 Monterey 或更高版本。对以前版本的支持已经停止。
Windows 系统:Go1.25 是最后一个包含对 32 位 Windows ARM 架构(GOOS=windows,GOARCH=arm) 支持的版本,这个移植版本存在问题(被称为“broken”)。 从 Go1.26 开始,这个架构支持将被移除,即不再支持在 32 位的 Windows ARM 系统上编译或运行 Go 程序。
Linux 系统:内核方面没有新的变化要求。
Panic 输出日志更简洁
当程序因为一个未处理的 panic(已被 recover 后又重新 panic)而退出时,打印的信息现在不会再重复 panic 值的文本内容。之前如果一个程序执行了 panic("PANIC"),然后通过 recover 捕获了这个 panic,接着又使用原始值重新 panic,他会打印:
panic: PANIC [recovered]
panic: PANIC
现在,程序将改为打印:
panic: PANIC [recovered, repanicked]
本次 Go1.25 主要就是将两次 panic 输出的语义信息合并到一行中,并避免重复显示 panic 值 "PANIC",让日志更简洁清晰。
ReadLinkFS、DirFS 支持获取符号链接的指向位置
在 Go1.25 中,引入了一个新的接口,称为 ReadLinkFS。这个接口提供了读取文件系统中符号链接(symbolic links)的能力:

ReadLinkFS 对应的接口如下:
type ReadLinkFS interface {
FS
// ReadLink returns the destination of the named symbolic link.
// If there is an error, it should be of type [*PathError].
ReadLink(name string) (string, error)
// Lstat returns a [FileInfo] describing the named file.
// If the file is a symbolic link, the returned [FileInfo] describes the symbolic link.
// Lstat makes no attempt to follow the link.
// If there is an error, it should be of type [*PathError].
Lstat(name string) (FileInfo, error)
}
ReadLinkFS 接口允许程序读取符号链接的目标路径。这意味着开发者可以通过该接口轻松地获取符号链接指向的实际文件或目录位置。同时还包括对 os.DirFS 的改进,使其实现了 ReadLinkFS 接口:

这意味着在使用 os.DirFS 时,用户也可以利用该接口来处理符号链接。
TypeAssert 支持对值进行断言
在日常 Go 程序进行反射时,我们一般是使用 reflect.ValueOf 方法来进行。但是经过性能测试,发现了一些问题。
基准测试代码:
func Benchmark(b *testing.B) {
// 通过反射创建一个可寻址的 time.Time 值
v := reflect.ValueOf(new(time.Time)).Elem()
// // 开启内存分配报告
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// 将反射值转为接口,再转回 time.Time
_ = v.Interface().(time.Time)
}
}
这段基准测试的目标是测量 v.Interface().(time.Time) 操作的性能和内存开销。
测试结果:
Benchmark-24 37673833 29.74 ns/op 24 B/op 1 allocs/op
需要重点关注的是:每次操作中发生了 1 次内存分配。
会有内存分配的原因是:
1.由于 v 是一个可寻址的 time.Time 值(通过 Elem() 获取),Go 的 reflect.Value.Interface() 会复制这个值。
2.复制的目的是:为了防止外部代码通过接口访问时直接修改原始数据(因为原始值是可寻址的,为了保持封装性和安全性)。
3.这个复制过程会导致 一次堆内存分配(alloc),从而出现了 24 B/op 和 1 allocs/op。
在前几年 tailscale 的 @Joe Tsai 大佬发现了这个问题,并提出了以下提案:

当时提案上希望增加 TypeAssert 的泛型方法:
// AssertTo is semantically equivalent to:
// v2, ok := v.Interface().(T)
func AssertTo[T any](v reflect.Value "T any") (T, bool)
新增该方法后,其作用语义上等同于 v.Interface().(T),但有效避免了 interface 装箱带来的复制和堆分配。从而在性能关键路径(例如:反射密集的场景)提供了更高效的替代方案。
后续的用法如下:
t, ok := reflect.AssertTo[time.Time](v "time.Time")
...
当然,最后到 Go1.25 函数的名字改了,改成以下函数签名:
func TypeAssert[T any](v Value "T any") (T, bool)
改名的原因是 rsc 认为:简单的 Assert 可能会与 C 风格的断言混淆。另外改名后有了 Type 前缀,也就不需要 To 后缀了。
trace 支持 FlightRecorder 模式
FlightRecorder 是什么
“Flight recording”(飞行记录)是一种技术,它将追踪数据保存在一个概念性的环形缓冲区中,并在需要时将其刷新。该技术的目的是捕获程序中有趣行为的追踪数据,即使事先无法预测这些行为会在什么时候发生。
例如:
.当 Web 服务未通过健康检查,或者处理某个请求的时间异常过长时。具体来说,Web 服务可以在这些异常实际发生时检测到,但程序员在部署时却无法准确预知它们会在何时出现。
.而在异常发生之后再启动追踪往往并没有什么用,因为此时程序已经执行过了那些“有趣”的部分。
Go FlightRecorder 落地
Go 在 runtime 中能够做 FlightRecorder,还是得益于之前在以下 issues#60773 对 trace 做过大修和性能优化:

再同样由 @Michael Knyszek 提起以下 FlightRecorder 的正式提案和实现内容:

在 Go 中,所有追踪数据将被组织为一系列自包含的分区(partitions)。这一实现上的变更为 Go 的执行追踪器添加类似“飞行记录”的能力提供了机会 —— 即始终保留至少一个分区,可在任意时刻快照保存。而正因为 issues#60773 的优化,Go trace 的性能有了大幅优化。长时间追踪下启用 FlightRecorder 已经足够轻量,不再是一个性能负担。从而在这个版本总算是实现了。以下是 runtime/trace 包中的新 API,用于启用 FlightRecorder。大家可以自行在 Go 程序中进行触发。
如下 API 代码:
package trace
type FlightRecorder struct {
...
}
func (*FlightRecorder) SetMinAge(d time.Duration)
func (*FlightRecorder) MinAge() time.Duration
func (*FlightRecorder) SetMaxBytes(bytes uint64)
func (*FlightRecorder) MaxBytes() uint64
func (*FlightRecorder) Start() error
func (*FlightRecorder) Stop() error
func (*FlightRecorder) Enabled() bool
func (*FlightRecorder) WriteTo(w io.Writer) (n int64, err error)
完整的 API 文档可以到 runtime/trace@master#FlightRecorder[1] 进行查看。
总结
本次主要是针对一些细致的特性进行了说明,像是大家可以检查自己 macOS 是否符合新版本的诉求。之前我就有因此将 OS 和集群版本升级的诉求。另外本次的 runtime/trace 的 FlightRecorder 模式支持将带来更多样的调试追踪的提效,反射中 TypeAssert 新方法支持带来的的性能优化、ReadLinkFS 和 DirFS 支持符合链接(软链)的读取,都是一些有实际效益的优化点。
参考资料
[1] runtime/trace@master#FlightRecorder: https://pkg.go.dev/runtime/trace@master#FlightRecorder