少数派报告----Edward's Webblog

Some raw thought.

Download as .zip Download as .tar.gz View on GitHub
30 January 2026

Go1.25 两个特性探索

by

今天探索一下 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 的排名也大幅下降,但这也并不是什么可怕的事情,程序设计语言是为我们服务的,而不是为了对比谁更流行。

tags: