从 Rob Pike 的提案到社区共识:Go 或将通过 new(v) 彻底解决指针初始化难题

精选Go深度内容!我的2025微专栏合集入口,扫码自选,开启进阶之旅👇。

大家好,我是Tony Bai。

在 Go 中创建一个指向基本类型(如 int 或 string)的指针,为何比创建一个指向结构体的指针更繁琐?这个长期存在的“人体工程学”问题,由 Go 语言的共同创造者之一 Rob Pike 在提案 #45624 中再次带入公众视野,并由此引发了一场长达数年、充满深度思辨的社区大讨论。最终,在权衡了多种方案的利弊后,社区逐渐形成共识,Go 提案委员会倾向于接受 new(v) 语法。本文将和大家一起回顾这场关于指针初始化的“十年之辩”,深入探讨各种方案的优劣,并解读为何 new(v) 可能成为最终赢家。

背景:一个困扰开发者多年的“小”问题

在 Go 中,我们可以用 p := &S{a: 3} 这样简洁的语法,一步到位地创建一个指向已初始化结构体的指针。但如果我们想创建一个指向 int 值 3 的指针,就必须写成:

a := 3
p := &a

这种不对称性在处理大量使用指针来表示“可选”字段的场景时(例如,与 JSON、Protobuf 或 AWS SDK 交互),会变得异常繁琐。开发者往往不得不在项目中定义或引入大量的辅助函数,如:

func StringPtr(s string) *string {
    return &s
}
// 还有 Int64Ptr, BoolPtr, Float64Ptr...

正如 @adonovan 在提案讨论中通过代码分析所展示的,这种模式在 Go 开源生态中极为普遍,存在数千个这样的辅助函数和数十万次的调用。这清晰地表明,语言层面提供一个更简洁的解决方案是众望所归。

方案之争:一场关于语法、语义与哲学的辩论

Rob Pike 的提案及其漫长的讨论过程,涌现了多种解决方案,每种方案都代表了一种不同的语言设计哲学。

方案一:扩展 & 操作符

这是最直观的想法,主要有两种变体:

  1. &T(v) (让类型转换变得可寻址): p := &int(3)。这是 Rob Pike 最初提出的方案之一。它利用了“类型转换必然会创建新值”这一语义,逻辑自洽。

  2. &v (让非地址表达式变得可寻址): p := &3 或 p := &time.Now()。这个方案更通用,但也最危险。正如 rsc 和其他核心成员指出的,这会产生严重的歧义。例如,&m[k] 在 m 是 slice 时是取地址,但在 m 是 map 时却变成了“拷贝值并取地址”,这会引入大量难以察觉的 bug。

由于存在严重的“最小惊动原则”问题,扩展 & 的方案最终未被采纳。

方案二:引入新的泛型内建函数

随着 Go 1.18 泛型的引入,一个显而易见的解决方案是提供一个泛型辅助函数。

// 可以是内置的,也可以是开发者自己写的
func ptr[T any](v T) *T {
    return &v
}

// 使用方式:
p := ptr(3)
p2 := ptr(time.Now())

这个方案得到了许多开发者的支持,因为它无需对语言规范做任何大的改动。然而,它的缺点也很明显:

  • 命名之争:应该叫 ptrrefaddrnewOf 还是 varOf?每种名称都有其支持者和反对者。例如,ptr 和 ref 可能会让人误以为是取现有变量的引用,而不是创建一个新的拷贝。

  • 标准库位置:这样一个基础的函数应该放在哪里?builtin?还是一个新的标准库包?这本身就是一个难题。

方案三:扩展 new 内建函数 (最可能的胜出者)

这是提案的核心,也是最终获得Go提案委员会青睐的方向。它同样有几种变体:

  1. new(T, v)new 接受一个可选的第二个参数用于初始化。例如 p := new(int, 3)。这非常明确,但缺点是类型 T 往往是冗余的,显得很“啰嗦”,例如 new(time.Duration, time.Second)

  2. new(v)new 可以直接接受一个值,并根据值的类型推断出要分配的指针类型。例如 p := new(3) 会创建一个 *int。这是最简洁的方案。

new(v) 的核心争议与共识

new(v) 的主要争议在于语法歧义。当看到 new(pkg.X) 时,读者无法仅从语法上判断 pkg.X 是一个类型(new(T))还是一个常量值(new(v))。

然而,经过深入讨论,提案委员会认为:

  • 这种歧义在实践中问题不大,因为绝大多数情况下,上下文足以让开发者区分类型和值。

  • 相比于 &v 带来的严重语义混乱,new(v) 的语法歧义是次要的、可接受的。

  • new 这个词本身就清晰地传达了“创建新事物”的意图,避免了 & 操作符的“拷贝还是引用”的混淆。

  • 考虑到 new(T) 的使用频率远低于 &T{},将其“回收”并赋予更强大的功能,是对语言的一次有益的“清理”。

最终,提案委员会倾向于接受 new(expr) 的形式。

new(expr) 将如何工作

根据讨论的共识,未来的 new(expr) 将遵循以下规则:

  • 基本用法p := new(3) 将创建一个 *int,其值为 3。s := new("hello") 将创建一个 *string,其值为 "hello"。

  • 类型推断: 对于无类型常量,将使用 Go 的默认类型规则(例如,整数默认为 int,浮点数默认为 float64)。

  • 显式类型: 如果需要指定不同于默认的类型,需要使用类型转换:p64 := new(int64(3))来创建一个*int64类型变量p64,而不是默认*int`指针类型变量。

  • 无上下文类型推断new(v) 不会根据赋值的上下文来推断类型。例如,var p *int64 = new(3) 将会编译失败,因为 new(3) 的类型是 *int,不能赋值给 *int64

结论:小改动,大便利

从 Rob Pike 最初的提案,到社区长达数年的激烈辩论,new(v) 的最终可能胜出是 Go 语言演进过程的一个缩影。它通过一个微小但精心设计的语法扩展,解决了困扰社区多年的一个普遍痛点。

这个决策过程本身,也充分体现了 Go 团队的设计哲学:

  1. 优先考虑语言的一致性和无歧义性,因此拒绝了看似更简洁但充满陷阱的 &expr 方案。

  2. 在不破坏兼容性的前提下,勇于重塑旧有特性,将使用率不高的 new 重新利用,赋予其更强大的生命力。

  3. 充分倾听并分析社区的真实数据,@adonovan 的大规模代码分析为该功能的需求提供了强有力的数据支撑。

虽然我们仍需等待该提案在未来某个 Go 版本中正式落地,但可以预见,当它到来时,我们代码库中那些重复的 Ptr 辅助函数将成为历史。这正是 Go 语言持续进化、不断提升开发者幸福感的魅力所在。

资料链接:https://github.com/golang/go/issues/45624

如果本文对你有所帮助,请帮忙点赞、推荐和转发

点击下面标题,阅读更多干货!

-  Go考古:创始人亲述Go语言的“创世纪”

Go:值与指针

Rob Pike的“抱怨”与Go的“解药”:直面软件膨胀的四大根源

Go语言之父的反思:我们做对了什么,做错了什么

Go语言之父谈Go编程语言与环境

Java屹立30年,Go的“少年壮志”如何续写辉煌?——来自Java之父的“长寿秘诀”

Go 1.25中值得关注的几个变化


🔥 你的Go技能,是否也卡在了“熟练”到“精通”的瓶颈期?

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?

  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?

  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

继《Go语言第一课》后,我的 《Go语言进阶课》 终于在极客时间与大家见面了!

我的全新极客时间专栏 《Tony Bai·Go语言进阶课》 就是为这样的你量身打造!30+讲硬核内容,带你夯实语法认知,提升设计思维,锻造工程实践能力,更有实战项目串讲。

目标只有一个:助你完成从“Go熟练工”到“Go专家”的蜕变! 现在就加入,让你的Go技能再上一个新台阶!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值