Sign in

Go 1.26 的 new(expr):现在可以接收值表达式

Go 代码里有一种很常见、也很不起眼的噪音:为了给一个标量值取地址,项目里到处都有 ptr.StringintPtrboolpproto.Int64 这类辅助函数。

它们通常不是业务抽象,只是为了绕过一个语法限制:在 Go 1.26 以前,new 只能接收类型,不能接收值表达式;而普通字面量、函数返回值、算术表达式又不能直接取地址。

当代码只是手写一点配置结构体时,这不算大问题。可一旦进入 API DTO、JSON Patch、protobuf、OpenAPI 生成客户端、云资源声明、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 开发者应该关心

很多序列化协议会用 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 Agent 自动补代码时也经常重复造一遍。

有了 new(expr),这段代码可以变成:

opts := UpdateOptions{
 Replicas: new(0),
 Enabled:  new(false),
}

少掉的不只是几行辅助函数,而是一层无意义的风格差异。

对 AI 生成 Go 代码有什么影响

AI 生成 Go 代码时,最容易出问题的地方往往不是复杂算法,而是项目局部风格。

同一个仓库里,可能已经有 ptr.To,也可能用 lo.ToPtr,还有些包里手写了 stringPtr。模型如果没完整读到上下文,就会按自己的训练偏好补一个新函数。代码能编译,但评审里会出现这种讨论:

func StringPtr(v string) *string { return &v }
func stringp(v string) *string { return &v }
func Ptr[T any](v T) *T { return &v }

这些差异不会改变业务行为,却会增加维护成本。

new(expr) 的价值在这里变得更明显:它把“创建一个初始化后的指针”收回语言内建表达式,让生成代码少依赖项目私有约定。

对 AI 工具来说,也更容易写出稳定规则:

这几条规则比“先搜一下项目里有没有指针辅助函数,然后猜应该用哪个”更可靠。

迁移不要一把梭

Go 1.26 也把这个变化接进了 go fix 的现代化能力里。已有项目可以先用工具找出典型的 new-like 辅助函数和调用点:

go fix -newexpr ./...

这个命令适合当成迁移起点,而不是自动合并理由。

建议按这样的节奏做:

git switch -c chore/go126-newexpr

go mod edit -go=1.26
go fix -newexpr ./...
go test ./...

git diff

重点看三类变化。

第一类,是包内私有辅助函数。

例如 intPtrstringPtrboolPtr 只在当前包里使用,替换成 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)' .

然后按范围处理:

如果团队有仓库级 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) 当成可选标量字段的默认表达方式,是一个很值得做的小改进。