Go1.25 两个特性探索
今天探索一下 Go 1.25 升级中的两个更新:greenteagc 和 json v2。
它们都被标记为 实验性,但这并不意味着“看看就好”。恰恰相反,这两个特性非常诚实地暴露了 Go 团队接下来几年真正想解决的问题:长期运行的系统成本,以及 数据结构在真实工程中的可控性。这两点是很多技术大牛们文章里反复强调过的:不要被语法糖迷惑,要盯着系统的真实复杂度。
先从 GC 说起。
Go 的 GC 一直是“工程权衡”的典型案例。最早期(Go 1.0~1.4),是 STW 的 mark-and-sweep,简单、直接,但在稍微大一点的服务里几乎不可用。Go 1.5 是一次断代式升级,引入了 并发标记、三色标记法,把 STW 压缩到可接受范围,这一版基本奠定了 Go 能在后端站住脚的基础。之后几年(1.6~1.18),优化重点主要集中在:缩短 pause、改进 write barrier、降低分配路径开销、让 GC 更“便宜”。
但有一个事实一直没变: GC 的整体模型是为“中等核数 + 单机服务”设计的。而现实 很多时候变成了在容器里跑服务、cgroup 限 CPU,而且现在64 核、96 核很常见,并且服务几周甚至几个月不重启
在这种环境下,传统 GC 的问题不是“慢”,而是 扩展性和可预测性。GC 线程之间的协调成本、全局状态的竞争、cache 抖动,会让 CPU 利用率看起来很高,但吞吐却不线性增长。greenteagc 正是在这个背景下出现的。
GreenTea GC 尝试重构 GC 的内部协作方式,让标记和回收在高核数下更均匀地摊开。官方没有在文档里给出太多算法细节,但从行为上可以感知一个变化:GC 更像一个低优先级、持续运行的后台系统,而不是周期性爆发的事件。因为gc 在效率是的目标一直都很明确:尽量降低 STW 的影响。
这次更新中的 GreenTea GC 一如既往地用新特性开关:
` GOEXPERIMENT=greenteagc go run main.go`
我们写一个刻意制造短生命周期对象的程序:
package main
import (
"fmt"
"runtime"
"time"
)
type Obj struct {
buf [1024]byte
}
func alloc() {
var xs []*Obj
for i := 0; i < 1_000_000; i++ {
xs = append(xs, &Obj{})
if i%100_000 == 0 {
xs = xs[:0]
}
}
}
func main() {
go func() {
for {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf(
"Heap=%dMB GC=%d Pause=%dus\n",
m.HeapAlloc/1024/1024,
m.NumGC,
m.PauseTotalNs/1000,
)
time.Sleep(time.Second)
}
}()
for {
alloc()
time.Sleep(200 * time.Millisecond)
}
}
在不开 greenteagc 时,GC 次数和 pause 累积会呈现明显的“锯齿型”;打开之后,GC 行为更平滑,CPU 峰值被摊薄。它不保证你的程序更快,但更可能 在负载升高时不突然变得不可预测。这一点,在云环境里非常值钱。
但也正因为这是对 GC 内部行为的深度改动,现在并不适合直接上生产。它更像是 Go 团队在问社区一个问题:“如果我们这样重构 GC,你们的程序会不会炸?” 目前看起来答案仍待时间来验证。
接下来再看 json v2。
encoding/json 是一个“成功但不可进化”的包。它的 API 在 Go 1.0 就定型了,当时追求的是“简单能用”。十多年过去,问题逐渐显现: tag 语义越来越复杂,omitempty 行为模糊,很多控制只能靠 hack 或反射;性能不是不能优化,而是 结构本身限制了优化空间。
json v2 的核心变化不是“更快”,而是 重新定义了编码模型。它把“如何编码”这件事从隐式规则,变成显式接口。
启用方式同样是实验开关:
GOEXPERIMENT=jsonv2 go run main.go
导入路径变为:
import "encoding/json/v2"
最直观的变化,是对字段行为的明确区分:
type User struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitzero"`
}
omitempty 和 omitzero 在 v1 里经常被混用,在 v2 中被明确区分: “空值”和“零值”是两种不同的语义,这一点在 API 设计里非常重要。
真正有意思的是自定义编码逻辑。v2 不再要求你实现整个 MarshalJSON,而是允许你参与编码过程的一部分:
type Secret string
func (s Secret) MarshalJSONV2(enc *json.Encoder) error {
if s == "" {
return enc.EncodeNull()
}
return enc.EncodeString("***")
}
这类代码在 v1 中要么写得很丑,要么根本做不到。v2 明显是在为 复杂协议、内部系统、长期维护的服务 做准备。
需要注意的是,json v2 并不是用来立刻替代 jsoniter 或 sonic 的。它当前更像是标准库层面的“地基重铺”。你可以在内部服务、实验项目中尝试,但不适合马上作为公共 SDK 的依赖。
从系统稳定性来说,这两个更新都不建议马上开始使用,但却值得现在开始关注和试验: greenteagc不要在生产环境开启,这应该是共识。但应该在压测环境里跑一跑,看看你的服务在未来 GC 模型下是否有异常行为。 至于json v2,可以在新项目或内部协议中试用,尤其是你对 JSON 行为有明确控制需求的时候。当然,实际中我们经常用的是第三方的 json 库,这时候也可以对比一下看看。
但我觉得go 语言现在的更新方向是比较明确的: Go 正在从“好用的工程语言”,转向“长期运行的系统平台”。
顺便提一句,前几天 go 的排名也大幅下降,但这也并不是什么可怕的事情,程序设计语言是为我们服务的,而不是为了对比谁更流行。



