Go 1.26 new(expr) 深度解析:告别AI生成可选字段的繁琐操作
Go 1.26 引入的 new(expr) 功能,乍一看只是个小语法糖,但它实际上精准地填补了 Go 工程实践中的一个长期空白。这个变化没有碘伏什么,只是让一件我们经常要做的事,变得更自然、更统一了。
在 Go 的代码里,有一种很常见但又略显“啰嗦”的模式:为了给一个简单的标量值(比如一个整数、一个字符串)取地址,我们不得不在项目里到处定义类似 intPtr、stringPtr、boolp 这样的辅助函数。这些函数本身没什么业务逻辑,纯粹是为了绕过语言的一个限制——在 Go 1.26 之前,new 关键字只能接收类型(如 new(int)),不能直接接收一个值表达式(比如 new(42) 是不合法的)。
当代码只是手写几个配置值时,这或许不算大问题。但一旦进入 API 数据模型、JSON Patch 操作、Protobuf 消息、OpenAPI 生成的客户端代码,或是云资源声明、AI Agent 工具参数这些场景,使用指针(*T)来表示“可选字段”的做法就会大量出现。更不用说,当 AI 开始参与编写代码时,这种“噪音”会被进一步放大:模型很容易在不同的文件里生成风格各异的 Ptr 辅助函数,让代码评审平白多出一堆与业务价值无关的风格讨论。
现在,Go 1.26 为这个问题提供了一个简洁的内建解决方案:new 现在可以接收一个值表达式了。这意味着,许多仅仅为了“把一个值变成指针”而存在的辅助函数,终于可以慢慢退出历史舞台了。
这次变化到底改了什么?
过去,new 的典型用法是创建一个类型的零值指针:
timeout := new(time.Duration)
*timeout = 30 * time.Second
它先分配内存并初始化为零值,然后我们还需要额外赋值才能得到想要的值。
从 Go 1.26 开始,你可以直接一步到位:
timeout := new(30 * time.Second)
new(expr) 会创建一个新变量,用表达式的值来初始化它,然后返回这个变量的指针。原来的 new(T) 语法依然有效,两者表达了不同的意图:
zero := new(int) // *int,指向 0
three := new(3) // *int,指向 3
如果表达式本身带有明确的类型信息,返回的指针类型也会随之确定:
id := new(int64(1001)) // *int64
name := new(“worker-a”) // *string
delay := new(5 * time.Second) // *time.Duration
这个能力看似简单,却正好击中了 Go 工程中一个微妙的痛点:如何优雅地初始化那些可选的标量字段。
为什么 Go 开发者应该关心?
在许多序列化协议和 API 设计中,常用 nil 指针表示“字段未设置”,而用一个非 nil 指针(即使其指向的值为零)来表示“字段被显式设置为了零值”。这两者语义完全不同。
来看一个典型的配置更新结构体:
type UpdateOptions struct {
Replicas *int `json:“replicas,omitempty”`
Enabled *bool `json:“enabled,omitempty”`
}
在这里,Replicas: nil 意味着“不更新副本数”,而 Replicas: new(0) 则意味着“将副本数明确设置为 0”。这种模式在各类 API、控制系统、云资源声明和模型参数中非常普遍。
在 Go 1.26 之前,调用方通常需要借助辅助函数:
func intPtr(v int) *int { return &v }
func boolPtr(v bool) *bool { return &v }
opts := UpdateOptions{
Replicas: intPtr(0),
Enabled: boolPtr(false),
}
或者引入一个泛型帮助函数:
func Ptr[T any](v T) *T { return &v }
opts := UpdateOptions{
Replicas: Ptr(0),
Enabled: Ptr(false),
}
这些函数本身没错,但它们给代码库引入了一层额外的、项目特有的约定。不同团队、不同代码生成器、甚至不同 AI 模型都可能创造出名字各异的版本,导致代码风格的不一致。
有了 new(expr),上面的代码可以变得更清晰、更标准:
opts := UpdateOptions{
Replicas: new(0),
Enabled: new(false),
}
减少的不仅仅是几行代码,更消除了那层无意义的风格差异。
对 AI 生成 Go 代码有什么影响?
AI 在生成 Go 代码时,最容易出问题的地方往往不是复杂的算法,而是项目局部的编码风格和约定。
同一个仓库里,可能已经存在 ptr.To、lo.ToPtr,或者手写的 stringPtr。如果 AI 模型没有完整地理解项目上下文,它可能会按照自己的训练偏好,生成一个新的辅助函数。代码虽然能编译,但评审时就会陷入这样的讨论:“我们该用哪个 Ptr 函数?”
new(expr) 的价值在这里更加凸显:它将“创建一个已初始化的指针”这个操作,收归为语言的内建表达式。这使得生成的代码减少了对项目私有约定的依赖。
对于 AI 工具来说,现在可以遵循更稳定、更简单的规则:
需要表达可选标量字段时,优先使用 new(expr)。
需要指向结构体复合字面量时,继续使用 &T{…}。
需要零值指针时,使用 new(T)。
不要为了标量值新增 Ptr 类的帮助函数。
这几条规则,远比让 AI 去“搜索并猜测应该使用项目里的哪个辅助函数”要可靠得多。
迁移不要一把梭
Go 1.26 也将这个特性集成到了 go fix 工具的现代化能力中。你可以用以下命令作为迁移的起点,来找出项目中那些可以被替换的类 new 辅助函数及其调用点:
go fix -newexpr ./…
不过,这个命令更适合作为参考,而不是直接全盘自动替换的理由。建议按以下节奏进行:
git switch -c chore/go126-newexpr
go mod edit -go=1.26
go fix -newexpr ./…
go test ./…
git diff
重点审查三类变化:
第一类,包内私有辅助函数。 例如只在当前包内使用的 intPtr、stringPtr。将它们替换为 new(expr) 没有兼容性负担,最适合优先清理。
第二类,已暴露的公共 API。 如果某个包导出了 Ptr[T] 这样的函数,并且外部用户已经在调用,不要因为内部迁移就贸然删除。可以先让内部代码改用 new(expr),公共函数则保留到下一个有明确弃用策略的版本。
第三类,生成的代码。 对于由 OpenAPI、Protobuf 或内部工具生成的代码文件,不要直接手动修改生成结果。正确的做法是更新生成器模板,让新生成的代码直接产出 new(expr),否则下一次生成就会覆盖你的手动修改。
代码评审要看语义,不只看更短
new(expr) 能让代码更简洁,但简洁本身不是唯一目标。使用可选字段时,最容易踩的坑就是混淆“缺省”和“显式设为零值”这两种完全不同的语义。
例如,在一个模型调用参数的结构体中:
type ModelCallOptions struct {
Temperature *float64 `json:“temperature,omitempty”`
MaxTokens *int `json:“max_tokens,omitempty”`
Stream *bool `json:“stream,omitempty”`
}
下面这段代码并不意味着“使用默认值”:
opts := ModelCallOptions{
Temperature: new(0.0),
MaxTokens: new(0),
Stream: new(false),
}
它表达的是三个字段都被显式设置了,只不过值恰好是零值。如果你真正想表达的是“让服务端使用其默认值”,那么应该保留字段为 nil:
opts := ModelCallOptions{}
因此,在评审 AI 生成的补丁或新代码时,不要只看到 new(0) 比 Ptr(0) 更干净就通过。真正需要确认的是:这里的业务逻辑到底需要的是一个显式的零值,还是应该什么都不传。这对于模型参数、资源配额、开关配置等场景至关重要,许多线上事故的根源正是混淆了“未设置”和“设为零”。
项目里可以怎么落地?
如果团队计划升级到 Go 1.26,可以将 new(expr) 纳入一个简单的工程约定。
首先,检查当前项目中的指针辅助函数:
rg ‘func .*Ptr|func .*ptr|func Ptr\[|ToPtr|pointer\.To|proto\.(String|Bool|Int|Int32|Int64)’ .
然后,可以按以下范围逐步处理:
- 新代码中,不再为标量可选字段新增私有的
Ptr帮助函数。 - 现有的私有辅助函数可以逐步替换为
new(expr)。 - 已经暴露的公共辅助函数先保留,等待合适的版本周期再考虑移除。
- 优先升级代码生成器模板,避免人工修改生成结果。
- 在AI 代码生成规则中,明确区分
nil、new(0)、new(false)所代表的不同语义。
如果团队使用仓库级的 AI Agent 指令,可以直接加入类似这样的说明:
When targeting Go 1.26+, use new(expr) for optional scalar pointer fields. Do not introduce Ptr helpers unless the package already exposes one as public API. Use nil when the field should be omitted.
这类规则不需要很复杂,却能显著减少 AI 生成代码时的风格漂移,让评审聚焦于真正的逻辑问题。
小结
new(expr) 是一个非常“Go”风格的改进:语法上改动很小,但在工程实践上非常实用。它没有改变 Go 关于指针、逃逸分析或零值的基本理念,只是补齐了一个长期缺失的常见表达方式。
过去,为了创建 *int、*string、*bool、*time.Duration 这样的指针,项目不得不引入一批辅助函数。现在,语言本身提供了一个统一的写法。
对于普通业务代码,它能减少 DTO 和配置结构体中的样板代码。对于有 AI 参与开发的团队,它还能减少生成代码的风格分歧,让代码评审回到更本质的问题上:这个字段到底应该被省略,还是应该被显式设置。
升级到 Go 1.26 时,不必为了这一个特性而大规模重写现有代码。但从新的代码和生成器模板开始,将 new(expr) 作为可选标量字段的默认表达方式,是一个值得采纳的、能提升代码一致性的小改进。