Go 1.26 新特性导览
Go 1.26 将于 2026 年 2 月正式发布,现在正是探索新特性的绝佳时机。官方发布的更新日志较为枯燥,因此我准备了一个交互式版本,通过大量示例展示具体变化和新行为。
本文基于 Go 作者团队的官方发布说明及 Go 源代码编写,遵循 BSD-3-Clause 许可证。本文并非详尽无遗的清单,请参阅官方发布说明以获取完整信息。
文中为各项特性提供了文档(𝗗)、提案(𝗣)、提交记录(𝗖𝗟)和作者(𝗔)的链接。建议查阅这些资料以了解设计动机、使用方式和实现细节。部分特性我还撰写了专门指南(𝗚)。
为保持简洁,示例中常省略错误处理。切勿在生产环境中照搬此做法 ツ
new(expr):支持表达式参数
过去,new 内建函数只能用于类型:
p := new(int)
*p = 42
fmt.Println(*p)
现在,它也可以接受表达式作为参数:
// 创建一个值为 42 的 int 变量的指针
p := new(42)
fmt.Println(*p)
若参数 expr 的类型为 T,则 new(expr) 会分配一个类型为 T 的变量,将其初始化为 expr 的值,并返回其地址(类型为 *T)。
该特性在使用结构体指针字段表示 JSON 或 Protobuf 中的可选值时尤为实用:
type Cat struct {
Name string `json:"name"`
Fed *bool `json:"is_fed"` // 猫是否被喂食?你永远无法确定
}
cat := Cat{Name: "Mittens", Fed: new(true)}
data, _ := json.Marshal(cat)
fmt.Println(string(data))
new 还可用于复合字面量:
s := new([]int{11, 12, 13})
fmt.Println(*s)
type Person struct{ name string }
p := new(Person{name: "alice"})
fmt.Println(*p)
甚至可用于函数调用:
f := func() string { return "go" }
p := new(f())
fmt.Println(*p)
但传入 nil 仍不被允许:
p := new(nil)
// 编译错误
📚 规范 • 💡 提案 #45624 • 🔧 CL 704935 等 • 👤 Alan Donovan
类型安全的错误检查:errors.AsType
新增的 errors.AsType 是 errors.As 的泛型版本:
// Go 1.13+
func As(err error, target any) bool
// Go 1.26+
func AsType[E error](err error) (E, bool)
它类型安全且更易使用:
// 使用 errors.As
var target *AppError
if errors.As(err, &target) {
fmt.Println("应用错误:", target)
}
// 使用 errors.AsType
if target, ok := errors.AsType[*AppError](err); ok {
fmt.Println("应用错误:", target)
}
AsType 在检查多种错误类型时特别方便,代码更简洁,且错误变量作用域限定在 if 块内:
if connErr, ok := errors.AsType[*net.OpError](err); ok {
fmt.Println("网络操作失败:", connErr.Op)
} else if dnsErr, ok := errors.AsType[*net.DNSError](err); ok {
fmt.Println("DNS 解析失败:", dnsErr.Name)
} else {
fmt.Println("未知错误")
}
errors.As 使用反射,若使用不当(如传入非指针或未实现 error 接口的类型)会导致运行时 panic:
// 错误用法:target 不是指针
var target AppError
if errors.As(err, &target) { ... } // 可能 panic
而 AsType 在编译期即可捕获此类错误:
// 编译错误:AppError 不是指针类型
if target, ok := errors.AsType[AppError](err); ok { ... }
性能方面,AsType 不使用反射,执行更快、内存分配更少:
BenchmarkAs-8 12606744 95.62 ns/op 40 B/op 2 allocs/op
BenchmarkAsType-8 37961869 30.26 ns/op 24 B/op 1 allocs/op
由于 AsType 能完全替代 As,推荐在新代码中直接使用 AsType。
📚 errors.AsType • 💡 提案 #51945 • 🔧 CL 707235 • 👤 Julien Cretel
“绿茶”垃圾回收器(Green Tea GC)
Go 1.26 默认启用了新的垃圾回收器(“绿茶”GC,最初在 1.25 中作为实验性功能引入),旨在提升多核现代计算机上的内存管理效率。
动机
传统 GC 将对象视为图中的节点、指针视为边,不考虑对象在物理内存中的布局。扫描时频繁跳转到远端内存地址,导致大量 CPU 缓存未命中。超过 35% 的扫描时间浪费在等待内存数据上,且随着 CPU 核心数增加,问题愈发严重。
实现
“绿茶”GC 从“处理器中心”转向“内存感知”。它不再逐个扫描对象,而是以 8 KiB 的连续内存块(称为 span) 为单位进行扫描,重点优化 512 字节以下的小对象(因其最常见且最难高效扫描)。
每个 span 根据其大小类别(size class)划分为等长槽位,仅存放同尺寸对象。例如,32 字节大小类别的 span 会被划分为多个 32 字节槽位,对象起始地址对齐槽位开头。这种固定布局使 GC 可通过简单地址运算快速定位对象元数据,无需逐个检查对象大小。
当发现需扫描的对象时,GC 先标记其在 span 中的位置,暂不立即扫描,而是等待同一 span 中积累多个待扫描对象后批量处理,避免重复访问同一内存区域。
为更好利用多核,GC 工作线程采用任务窃取机制:每个线程拥有本地 span 队列,空闲时可从其他繁忙线程队列中“窃取”任务。这种去中心化设计消除了全局任务队列瓶颈,减少核心间竞争。
在 amd64 架构上,“绿茶”GC 还利用向量化 CPU 指令批量处理内存 span(当对象数量足够时)。
性能
Go 团队预计,在重度依赖 GC 的真实程序中,GC 开销可降低 10–40%;在支持 AVX-512 的 CPU(如 Intel Ice Lake、AMD Zen 4+)上,还可额外降低 10%。
⚠️ 目前尚无公开的最新版“绿茶”GC 基准测试结果,我也未能构建有效的合成基准测试。
该 GC 默认启用。如需使用旧版 GC,可在构建时设置 GOEXPERIMENT=nogreenteagc(该选项预计在 Go 1.27 中移除)。
💡 提案 #73581 • 👤 Michael Knyszek
更快的 cgo 与系统调用
Go 运行时中,处理器(P) 是执行 goroutine(G)所需的资源。线程(M)必须先获取 P 才能运行 G。
P 有多种状态:_Prunning(运行中)、_Pidle(空闲)、_Pgcstop(GC 暂停)等。
此前,当 goroutine 执行系统调用或 cgo 调用时,P 会进入 _Psyscall 状态。Go 1.26 移除了该状态,改为直接检查绑定到 P 的 goroutine 是否处于系统调用中。
此举减少了运行时开销,简化了 cgo 和系统调用的代码路径。官方称 cgo 运行时开销降低 30%,提交记录显示性能提升 18%:
CgoCall-64: 43.69n → 35.83n (-17.99%)
我在 Apple M1 上的本地测试结果更显著:
CgoCall-8: 28.55n → 19.02n (-33.40%)
CgoCallWithCallback-8: 72.76n → 57.38n (-21.14%)
几何平均: -27.53%
系统调用也有提升:
Syscall-8: 195.6n → 178.1n (-8.95%)
🔧 CL 646198 • 👤 Michael Knyszek
更快的内存分配
Go 运行时现在为 1–512 字节的小对象提供了专用的内存分配函数。通过跳转表(jump table) 快速选择对应尺寸的分配器,而非使用单一通用实现。
📝 注:尽管发布说明称“编译器将生成对尺寸专用分配例程的调用”,但实际上编译器仍调用通用
mallocgc函数,由其在运行时分发至专用分配器。
该优化使小对象分配成本最高降低 30%,整体分配密集型程序预计提升 ~1%。
我的本地基准测试(Apple M1)结果如下:
Alloc1-8: 8.190n → 6.594n (-19.48%)
Alloc8-8: 8.648n → 7.522n (-13.02%)
Alloc64-8: 15.70n → 12.57n (-19.88%)
Alloc128-8: 56.80n → 17.56n (-69.08%)
Alloc512-8: 81.50n → 55.24n (-32.23%)
几何平均: -34.83%
该优化默认启用。可通过 GOEXPERIMENT=nosizespecializedmalloc 禁用(预计 Go 1.27 移除)。
🔧 CL 665835 • 👤 Michael Matloob
向量化操作(实验性)
新增 simd/archsimd 包提供对架构特定向量化指令(SIMD)的底层访问,目前仅支持 amd64。
因不同 CPU 的 SIMD 指令差异巨大,Go 团队选择先提供低层、架构专用 API,让高级用户能在主流服务器平台(amd64)上立即使用 SIMD。
包中定义了与硬件寄存器匹配的向量类型,如 Int8x16(128 位,16 个 8 位整数)、Float64x8(512 位,8 个 64 位浮点数),支持 128/256/512 位宽向量。
操作以方法形式定义,通常零开销映射到硬件指令。例如,使用 SIMD 加法:
func Add(a, b []float32) []float32 {
if len(a) != len(b) { panic("长度不同") }
if !archsimd.X86.AVX512() { return fallbackAdd(a, b) }
res := make([]float32, len(a))
n := len(a)
i := 0
// SIMD 循环:每次处理 16 个元素
for i <= n-16 {
va := archsimd.LoadFloat32x16Slice(a[i:i+16])
vb := archsimd.LoadFloat32x16Slice(b[i:i+16])
vSum := va.Add(vb) // 对应 VADDPS 汇编指令
vSum.StoreSlice(res[i:i+16])
i += 16
}
// 标量尾部:处理剩余元素
for ; i < n; i++ { res[i] = a[i] + b[i] }
return res
}
常见操作包括:
- 加载/存储:
Load/Store - 算术:
Add,Sub,Mul,Div,DotProduct - 位运算:
And,Or,Not,Xor,Shift - 比较:
Equal,Greater,Less,Min,Max - 转换:
As,SaturateTo,TruncateTo - 掩码:
Compress,Masked,Merge - 重排:
Permute
⚠️ 该包仅使用 AVX 指令,不包含 SSE。
基准测试(AMD EPYC)显示 SIMD 版本吞吐量提升数十倍:
AddSIMD/1m-2: 127351 MB/s vs AddPlain/1m-2: 11932 MB/s
该包为实验性,需通过 GOEXPERIMENT=simd 启用。
📚 simd/archsimd • 💡 提案 #73787 • 🔧 多个 CL • 👤 Junyang Shao 等
安全模式(Secret Mode,实验性)
加密协议(如 WireGuard/TLS)要求前向保密(forward secrecy):即使长期密钥泄露,历史会话也不应被解密。这要求临时密钥(ephemeral key) 在握手完成后立即从内存清除。
Go 运行时不保证内存何时清零,敏感数据可能残留在堆或栈中(如 core dump)。开发者常需用反射等不可靠“黑科技”手动清零。
Go 1.26 引入 runtime/secret 包解决此问题。secret.Do(func()) 可在安全模式下执行函数,结束后立即清零其使用的寄存器和栈;堆分配则在 GC 判定不可达后清零。
secret.Do(func() {
// 生成临时密钥并完成会话协商
})
示例:安全派生会话密钥
func DeriveSessionKey(peerPublicKey *ecdh.PublicKey) (*ecdh.PublicKey, []byte, error) {
var pubKey *ecdh.PublicKey
var sessionKey []byte
var err error
secret.Do(func() {
// 1. 生成临时私钥(高度敏感!)
privKey, e := ecdh.P256().GenerateKey(rand.Reader)
if e != nil { err = e; return }
// 2. 计算共享密钥(同样高度敏感!)
sharedSecret, e := privKey.ECDH(peerPublicKey)
if e != nil { err = e; return }
// 3. 派生最终会话密钥
sessionKey = performHKDF(sharedSecret)
pubKey = privKey.PublicKey()
})
return pubKey, sessionKey, err // “配方”已被销毁
}
此处临时私钥和共享密钥如同“有毒废料”——必要但危险。若残留在内存中,攻击者可利用其重放解密历史流量。
secret.Do 确保这些中间值在会话密钥生成后永久销毁,即使未来服务器被攻破,该会话也无法被解密,从而保障前向保密。
⚠️ 当前仅支持 Linux(amd64/arm64)。不支持平台直接调用函数。函数内启动 goroutine 会 panic(Go 1.27 修复)。
该包主要面向加密库开发者,普通应用应使用已集成 secret.Do 的高层库。
需通过 GOEXPERIMENT=runtimesecret 启用。
📚 runtime/secret • 💡 提案 #21865 • 🔧 CL 704615 • 👤 Daniel Morsing
无 Reader 的加密 API
现有加密 API(如 ecdsa.GenerateKey、rand.Prime)常接受 io.Reader 作为随机源:
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
prim, _ := rand.Prime(rand.Reader, 64)
但 API 未承诺如何使用 reader 中的字节。底层算法变更可能导致读取序列/数量变化,若应用依赖特定行为,升级后可能出错。
Go 团队采取大胆方案:大多数加密 API 现在忽略 io.Reader 参数,始终使用系统随机源(crypto/internal/sysrand.Read):
// reader 参数不再使用,可传 nil
key, _ := ecdsa.GenerateKey(elliptic.P256(), nil)
prim, _ := rand.Prime(nil, 64)
受影响的子包包括:
crypto/dsacrypto/ecdhcrypto/ecdsacrypto/randcrypto/rsa
⚠️
ed25519.GenerateKey(rand)仍使用提供的 reader;若为nil,则使用内部安全随机源(非crypto/rand.Reader)。
为支持确定性测试,新增 testing/cryptotest 包:
func Test(t *testing.T) {
cryptotest.SetGlobalRandom(t, 42) // 设置全局确定性随机源
p1, _ := rand.Prime(nil, 32)
// 所有测试运行将生成相同数字
}
SetGlobalRandom 影响 crypto/rand 及所有 crypto/* 包的隐式随机源。
可通过 GODEBUG=cryptocustomrand=1 恢复旧行为(未来版本将移除)。
📚 testing/cryptotest • 💡 提案 #70942 • 🔧 CL 724480 • 👤 Filippo Valsorda 等
Goroutine 泄漏分析(实验性)
Goroutine 泄漏指 goroutine 因通道等同步原语无限阻塞,而程序其他部分仍在运行。例如:
func leak() <-chan int {
out := make(chan int)
go func() { out <- 42 }() // 若无人读取,则泄漏
return out
}
func main() { leak() } // 内部 goroutine 永久阻塞
泄漏不像死锁会 panic,也长期缺乏工具检测。
Go 1.24 引入 synctest 包用于测试时检测泄漏。Go 1.26 新增实验性 goroutineleak pprof 分析器,用于生产环境报告泄漏:
func main() {
prof := pprof.Lookup("goroutineleak")
leak()
time.Sleep(50 * time.Millisecond)
prof.WriteTo(os.Stdout, 2) // 输出泄漏的 goroutine 栈跟踪
}
其实现原理:利用 GC 标记阶段,从可运行 goroutine 出发,标记所有可达的同步对象及等待其上的阻塞 goroutine。剩余无法到达的阻塞 goroutine 即视为泄漏。
📚 runtime/pprof • 📘 泄漏检测指南 • 💡 提案 #74609 • 🔧 CL 688335 • 👤 Vlad Saioc
需通过 GOEXPERIMENT=goroutineleakprofile 启用,并可通过 /debug/pprof/goroutineleak HTTP 端点访问。
Goroutine 指标
runtime/metrics 新增指标,提供 goroutine 调度的细粒度洞察:
- 程序启动以来创建的 goroutine 总数
- 各状态 goroutine 数量
- 活跃线程数
完整列表:
/sched/goroutines-created:创建总数/sched/goroutines/not-in-go:阻塞于系统调用/cgo 的 goroutine/sched/goroutines/runnable:就绪但未运行的 goroutine/sched/goroutines/running:正在运行的 goroutine/sched/goroutines/waiting:等待 I/O 或同步原语的 goroutine/sched/threads/total:Go 运行时拥有的活跃线程数
这些指标可关联生产问题:
waiting持续增长 → 锁竞争not-in-go过高 → goroutine 卡在 syscall/cgorunnable积压 → CPU 不足
通过 metrics.Read 读取:
sample := []metrics.Sample{{Name: "/sched/goroutines/running:goroutines"}}
metrics.Read(sample)
fmt.Printf("Running: %v\n", sample[0].Value.Uint64())
📚 runtime/metrics • 💡 提案 #15490 • 🔧 多个 CL • 👤 Michael Knyszek
反射迭代器
reflect 包新增迭代器方法:
Type.Fields()/Type.Methods():遍历结构体字段/方法Type.Ins()/Type.Outs():遍历函数参数/返回值Value.Fields()/Value.Methods():遍历值的字段/方法(同时返回类型信息和值)
// 遍历 http.Client 字段
typ := reflect.TypeFor[http.Client]()
for f := range typ.Fields() {
fmt.Println(f.Name, f.Type)
}
// 遍历值的字段(含值)
client := &http.Client{}
val := reflect.ValueOf(client)
for f, v := range val.Elem().Fields() {
fmt.Printf("- %s (%s)\n", f.Name, v.Kind())
}
相比旧版 for i := 0; i < typ.NumField(); i++ 更简洁。
Peek into a buffer
bytes.Buffer 新增 Peek(n) 方法,返回缓冲区前 n 字节但不移动读取位置:
buf := bytes.NewBufferString("I love bytes")
sample, _ := buf.Peek(1) // "I"
buf.Next(2) // 跳过 "I "
sample, _ = buf.Peek(4) // "love"
若缓冲区不足 n 字节,返回 io.EOF。
返回的切片指向缓冲区内存,在下次读写前有效。直接修改会影响后续读取:
buf := bytes.NewBufferString("car")
sample, _ := buf.Peek(3) // "car"
sample[2] = 't' // 修改底层缓冲区
data, _ := buf.ReadBytes(0) // "cat"
📚 Buffer.Peek • 💡 提案 #73794 • 🔧 CL 674415 • 👤 Ilia Choly
进程句柄(Process Handle)
os.Process 内部在支持的操作系统(Linux 使用 pidfd,Windows 使用句柄)上使用进程句柄而非 PID,确保方法始终作用于同一进程(避免 PID 复用问题)。
Go 1.26 新增 Process.WithHandle 方法访问该句柄:
proc, _ := os.StartProcess("/bin/echo", []string{"hello"}, nil)
proc.WithHandle(func(handle uintptr) {
fmt.Println("handle =", handle)
})
句柄在回调函数返回前保证有效(即使进程已终止)。仅支持 Linux 5.4+ 和 Windows,其他系统返回 os.ErrNoHandle。
📚 Process.WithHandle • 💡 提案 #70352 • 🔧 CL 699615 • 👤 Kir Kolyshkin
信号作为上下文取消原因
signal.NotifyContext 返回的上下文被取消时,其 context.Cause(ctx) 现在会显示具体信号(此前仅显示 "context canceled"):
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
// ...
fmt.Println("cause =", context.Cause(ctx)) // 输出 "interrupt"
返回类型 signal.signalError 基于字符串,仅提供信号的字符串表示,非 os.Signal 值。
📚 signal.NotifyContext • 🔧 CL 721700 • 👤 Filippo Valsorda
比较 IP 子网
netip.Prefix 新增 Compare 方法,便于排序 CIDR 前缀:
prefixes := []netip.Prefix{
netip.MustParsePrefix("10.1.0.0/16"),
netip.MustParsePrefix("203.0.113.0/24"),
// ...
}
slices.SortFunc(prefixes, netip.Prefix.Compare)
排序规则:
- 有效性(无效 < 有效)
- 地址族(IPv4 < IPv6)
- 网络地址(
10.0.0.0/16<10.1.0.0/16) - 前缀长度(
/8</16) - 原始 IP(
10.0.0.0/8<10.0.0.1/8)
符合 Python netaddr.IPNetwork 和 IANA 标准。
📚 Prefix.Compare • 💡 提案 #61642 • 🔧 CL 700355 • 👤 database64128
支持上下文的拨号
net 包顶层函数(DialTCP 等)因早于 context.Context 设计,不支持取消。而 net.Dialer.DialContext 虽支持取消,但因地址解析和网络分发开销,效率较低。
Go 1.26 为 net.Dialer 新增上下文感知的专用拨号方法:DialTCP、DialUDP 等,兼具专用函数的效率和 DialContext 的取消能力:
var d net.Dialer
raddr := netip.MustParseAddrPort("127.0.0.1:12345")
conn, err := d.DialTCP(ctx, "tcp", netip.AddrPort{}, raddr)
📚 net.Dialer • 💡 提案 #49097 • 🔧 CL 490975 • 👤 Michael Fraenkel
伪造 example.com
httptest.Server 的默认证书已包含 example.com。此前,Server.Client() 会因证书不受信任而拒绝连接真实 example.com。
Go 1.26 起,Server.Client() 会将 example.com 及其子域名的请求重定向到测试服务器:
srv := httptest.NewTLSServer(handler)
resp, _ := srv.Client().Get("https://example.com") // 实际请求测试服务器
📚 Server.Client • 🔧 CL 666855 • 👤 Sean Liao
优化 fmt.Errorf
此前,对纯字符串使用 fmt.Errorf("foo") 比 errors.New("foo") 多分配内存。有人建议无格式化时改用 errors.New。
Russ Cox 反对:“统一使用 fmt.Errorf 更简洁,无需根据参数切换函数。”
Go 1.26 优化后,fmt.Errorf 对非逃逸错误0 分配,逃逸错误1 分配,与 errors.New 持平。CPU 开销也大幅缩小(逃逸错误:64ns → 25ns vs 21ns)。
📚 fmt.Errorf • 🔧 CL 708836 • 👤 thepudds
优化 io.ReadAll
此前 io.ReadAll 在扩容结果切片时分配大量中间内存。新实现使用指数增长的中间切片,最后复制到精确大小的最终切片。
对 65KiB 输入,速度提升 2 倍,内存使用减半。最终切片容量更精确,避免长期持有无用内存。
📚 io.ReadAll • 🔧 CL 722500 • 👤 thepudds
多日志处理器(Multiple Log Handlers)
slog 包(Go 1.21 引入)新增 MultiHandler,可同时向多个目标(如 stdout 和文件)输出日志:
stdoutHandler := slog.NewTextHandler(os.Stdout, nil)
file, _ := os.OpenFile("/tmp/app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
fileHandler := slog.NewJSONHandler(file, nil)
multiHandler := slog.NewMultiHandler(stdoutHandler, fileHandler)
logger := slog.New(multiHandler)
logger.Info("login", slog.String("name", "whoami"))
MultiHandler 依次调用所有启用的处理器。若任一处理器返回错误,使用 errors.Join 合并所有错误。
📚 slog.MultiHandler • 💡 提案 #65954 • 🔧 CL 692237 • 👤 Jes Cok
测试产物(Test Artifacts)
测试或基准测试生成的文件(如日志、内存转储)对远程调试(如 CI)至关重要。
Go 1.26 新增 T.ArtifactDir()、B.ArtifactDir()、F.ArtifactDir() 方法,返回测试专属目录:
func TestFunc(t *testing.T) {
dir := t.ArtifactDir()
logFile := filepath.Join(dir, "app.log")
os.WriteFile(logFile, []byte("ERROR: Connection failed\n"), 0644)
}
使用 go test -artifacts 时,产物保存在 -outputdir 指定目录(默认当前目录)下的 _artifacts/ 子目录中。未指定 -artifacts 时,产物存于临时目录并在测试后删除。
每个测试/子测试有独立产物目录(同级而非嵌套)。
📚 T.ArtifactDir • 💡 提案 #71287 • 🔧 CL 696399 • 👤 Damien Neil
现代化的 go fix
go fix 命令曾是针对古老 Go 特性的重写工具集。Go 1.26 起,它基于 Go 分析框架(与 go vet 相同)重构。
go vet:报告问题(修复建议不一定安全)go fix:现代化代码(修复始终安全,但不一定表示代码有问题)
用法:
go fix -diff . # 显示补丁而非应用
go fix -forvar . # 仅运行 forvar 分析器
go fix -omitzero=false . # 运行除 omitzero 外的所有分析器
示例:将循环替换为 slices.Contains:
// 修复前
func find(s []int, x int) bool {
for _, v := range s {
if x == v { return true }
}
return false
}
// 修复后
func find(s []int, x int) bool {
return slices.Contains(s, x)
}
总结
Go 1.26 是史上最大规模的发布,理由充分:
- 实用更新:增强的
new、类型安全错误检查、goroutine 泄漏检测器 - 性能提升:新 GC、更快的 cgo/内存分配、优化的
fmt.Errorf/io.ReadAll - 体验改进:多日志处理器、测试产物、现代化
go fix - 专业实验特性:SIMD 支持、前向保密安全模式
此外,Go 1.25 引入的实验性 json/v2 包仍在开发中,可通过 GOEXPERIMENT=jsonv2 启用。
总而言之,这是一次卓越的发布!
本文链接:https://360us.net/article/110.html