标签 Go 下的文章

沉睡 8 年的提案被唤醒:Go 语言真的要引入“不可变类型”了吗?

本文永久链接 – https://tonybai.com/2026/02/09/go-immutable-types-8-year-dormant-proposal-awakened

大家好,我是Tony Bai。

2026 年 2 月 4 日,在 Go 语言规范团队的最新一次“语言变更评审会议”纪要中,一个尘封已久的 Issue 赫然在列:proposal: spec: immutable type qualifier #27975

这个提案最初提交于 2018 年,那是“Towards Go 2”口号喊得最响亮的年代。当时的 Go 社区正沉浸在对泛型、错误处理和不可变性的热烈讨论中。然而,随着泛型的落地,关于不可变性的声音似乎逐渐微弱。

如今,这个提案被重新摆上台面,是否意味着 Go 语言在完成泛型这一宏大叙事后,终于要向“数据竞争”和“防御性编程”这两个顽疾开刀了?

今天,我们就来看看复盘这份长达 8 年的提案,剖析一下“不可变性”对 Go 意味着什么,以及它面临的巨大挑战。

痛点:防御性拷贝的代价

在 Go 1.x 的世界里,我们为了保证数据的安全性,往往需要付出高昂的代价。

假设你有一个包含敏感配置的结构体,你想把它暴露给其他包,但又不希望它被修改:

type Config struct {
    Servers []string
    // ...
}

// 现在的做法:为了安全,必须返回拷贝
func (c *Config) GetServers() []string {
    out := make([]string, len(c.Servers))
    copy(out, c.Servers)
    return out
}

这种“防御性拷贝”带来了两个严重问题:

  1. 性能损耗:每次访问都要分配内存和复制数据,对于热点路径是不可接受的。
  2. 语义模糊:如果我不拷贝,直接返回 c.Servers,调用者能不能改?文档说不能改,但这只是君子协定,编译器不会阻止手滑的程序员。

正如提案作者 romshark 所言:“我们现在的做法,要么是不安全的(直接返回指针),要么是低效的(防御性拷贝)。”

而不可变类型(Immutable Types)的引入,旨在提供第三种选择:既安全,又高效。

提案核心:immut 限定符

NO.27975 提案的核心思想非常直接:引入一个新的类型限定符(最初建议重载 const,后倾向于引入immut ),让编译器来强制执行“只读”契约。

想象一下这样的 Go 代码:

// 定义一个只读的切片类型
func ProcessData(data immut []byte) {
    // 读取是 OK 的
    fmt.Println(data[0]) 

    // 修改是编译错误的!
    // data[0] = 'X' // Compile Error: cannot assign to immutable type
}

在这个愿景中,不可变性是类型系统的一部分。

  • 赋值限制:你不能把一个 immut 类型的变量赋值给一个 mut(可变)类型的变量,这防止了“权限逃逸”。
  • 传递性:如果一个结构体是不可变的,那么它字段指向的所有数据(如切片、映射、指针)也自动变为不可变。

这看起来很像 Rust 的 & (immutable reference) 和 &mut (mutable reference),或者 C++ 的 const。但 Go 社区的讨论,揭示了这背后远比想象中复杂的工程难题。

社区激辩:理想与现实的碰撞

这份提案下的讨论区,堪称 Go 语言设计哲学的“修罗场”。Ian Lance Taylor, Rob Pike 等核心大佬纷纷下场,与社区开发者展开了长达数年的拉锯战。

const 污染

这是 Ian Lance Taylor 最担心的问题。如果你把一个底层函数的参数标记为 immut,那么所有调用它的上层函数,为了传递这个参数,往往也需要把自己的变量标记为 immut。

这种“传染性”会导致代码库中充斥着 immut 关键字。更糟糕的是,如果你以后需要修改底层函数,让它对数据进行一点点修改,你需要修改整个调用链上的类型签名。这在 C++ 中被称为“const correctness”的噩梦。

io.Writer 的尴尬

bcmills 提出了一个极其尖锐的兼容性问题:现有的 io.Writer 接口定义是 Write(p []byte)。

  • 如果我们把 p 改成 immut []byte,那么现有的所有 Write 方法实现都会破坏兼容性。
  • 如果我们不改,那么即使我手里有一个只读的切片,我也没法把它传给 io.Writer,因为类型不匹配。

这似乎陷入了一个死循环:要么破坏所有现有代码,要么新特性无法与标准库兼容。

所谓“不可变”,到底是谁不可变?

jimmyfrasche 指出了一个微妙的语义陷阱。

在 C++ 中,const T& 只是意味着“我不可以通过这个引用去修改它”(Read-only View),并不意味着“这个数据本身不会变”。因为可能还有另一个非 const 的指针指向同一块内存,并且正在修改它。

如果是前者(只读视图),它无法解决并发安全问题(数据竞争依然存在)。如果是后者(真正的内容不可变),那么 Go 必须引入一套类似 Rust 的所有权(Ownership)系统来保证“没有其他人在写”。这对于 Go 来说,改动太大了。

为何现在重提?

既然困难重重,为何在 2026 年的今天,这个提案又被翻出来了?

我认为有几个关键因素:

首先,泛型的“降维打击”。以权限泛型(Permission Genericity)化解兼容性死结。

前面提到了,在 Go 1.18 泛型落地之前,不可变性提案面临着一个被称为“io.Writer 陷阱”的致命矛盾:如果将 io.Writer.Write(p []byte) 改为接受 immut []byte,将导致全世界现有的实现代码因签名不匹配而崩溃;如果不改,只读数据又无法直接传入。

泛型的引入为这一难题提供了全新的解题思路。通过类型约束中的联合类型(Union Types),我们可以实现所谓的“权限泛型性”。这意味着 mutability(可变性)不再是一个硬编码的死结,而可以作为一个类型参数(Type Parameter)来处理。

想象一下,我们可以利用泛型约束定义一个覆盖“可变”与“不可变”两种状态的超集:~[]byte | ~immut []byte。下面是在这种模式下的一个泛型化的Writer接口:

// 这是一个设想中的“权限泛型”接口
type Writer[T ~[]byte | ~immut []byte] interface {
    Write(p T) (n int, err error)
}

泛型化的 Write[T ~[]byte | ~immut []byte](p T) 方法,在逻辑上可以产生如下影响:

  1. 权限无关的调用:由于约束涵盖了两种类型,调用者现在可以合法且安全地将 immut []byte 传给标准库函数,解决了“只读数据无法写入”的窘境。
  2. 非破坏性的兼容:对于现有的实现者(如 bytes.Buffer),其原本定义的 Write([]byte) 签名可以被视为该泛型约束的一个特化实例。编译器可以在不改动任何旧代码、不引入任何运行时开销的前提下,在静态分析阶段完成权限的自动适配与校验。

其次,性能压力的倒逼。

随着 Go 在高性能领域的应用越来越深(如数据库、AI 推理),对于“零拷贝”的需求越来越强烈。能够安全地共享内存,是提升性能的关键。

最后是安全性需求。

在并发编程中,数据竞争依然是 Go 程序的头号杀手。go vet 和 race detector 虽然好用,但它们是运行时的、滞后的。社区渴望一种编译期的保证。

未来的可能性:温和的演进

虽然完全的“不可变类型”可能依然很难落地,但我们可以期待一些更温和的替代方案:

  • 只读视图 (Read-only Views):不是引入新的关键字,而是引入一种新的泛型类型 ReadOnly[T],或者编译器内置的 view 类型。
  • 纯函数检查:引入一种机制,标记某些函数是“无副作用”的,从而允许编译器进行更激进的优化。
  • 静态分析增强:不改变语言规范,而是通过更强大的 vet 工具,利用注释或特定命名约定,来静态检查不可变性约束。

小结

NO.27975 提案的“复活”,是一个信号。它表明 Go 团队并没有满足于现状,依然在探索如何在保持“简单”这一核心价值观的同时,赋予语言更强的表达力和安全性。

无论最终结果如何,这都是 Go 语言演进史上值得铭记的一笔。它提醒我们:在软件工程中,没有免费的午餐,每一个简单的特性背后,都是无数次复杂的权衡。


你支持引入 immut 吗?

面对“性能”与“简单”的博弈,你是否愿意为了消除数据竞争而接受 immut 带来的“类型传染”?在你的项目中,是否也曾深受“防御性”的性能困扰?

欢迎在评论区分享你的看法,或者聊聊你最期待的 Go 演进方向!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


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

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

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

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

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


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

数据打脸刻板印象:Go 的“样板代码”竟然和 Rust 一样多?

本文永久链接 – https://tonybai.com/2026/02/08/go-boilerplate-code-vs-rust-data-refutes-stereotypes

大家好,我是Tony Bai。

在编程语言的鄙视链中,Go 语言常常因为其“繁琐”而饱受诟病。

“if err != nil 写断手”、“缺乏语法糖”、“到处都是重复的样板代码”…… 这些似乎已经成为了 Go 的标签。

相比之下,Rust 往往被视为“表达力”的代表,拥有强大的宏、模式匹配和类型系统,能够用更少的代码做更多的事。

然而,Ben Boyter 最近的一项硬核研究,通过分析 GitHub 上各语言 Top 100 仓库(总计约 4 亿行代码),得出了一个令编程语言社区大跌眼镜的结论:

在代码重复率和“样板代码”密度上,Go 和 Rust 几乎处于同一水平线。

不仅是行数:ULOC 指标

传统的 SLOC(源代码行数)往往无法真实反映项目的复杂度和冗余度。Ben Boyter 使用了他开发的工具 scc 中的一个特殊指标:ULOC (Unique Lines of Code,唯一代码行数)

ULOC 指标并非简单的“全量去重”,而是通过剥离“结构性噪音”来更精准地衡量系统的真实复杂度。其计算逻辑如下:

  • 剔除结构化冗余:不仅排除了空行,还排除了单纯的闭合大括号行(})以及在不同文件中大量重复出现的公共引用代码(如 include 或 import)。
  • 过滤文件级模板:有效识别并扣除在项目中每个文件顶部几乎完全相同的 License(许可证)声明头,避免这些非逻辑性的“样板文字”虚增代码总量。
  • 计入注释成本:与传统 SLOC 不同,ULOC 会保留注释统计。作者认为,注释与代码一样需要同等的维护精力,反映了开发者的思考过程,因此属于“有效工作量”。

通过这种方式计算出的 Dryness(干度),代表了剔除“语法支架”和“版权模板”后,真正的业务逻辑与注释在代码中的占比。百分比越高,说明重复代码越少,信息密度越高;百分比越低,说明“样板代码”或重复结构越多。

令人震惊的对比:Go vs Rust

让我们直接看数据(数据来源:GitHub Top 100 仓库分析,2026年2月):

发现了吗?Rust (60.5%) 和 Go (58.78%) 的差距微乎其微,甚至可以说在统计学上是等价的。

Ben Boyter 在文章中坦言,他之前也持有“Go 的样板代码比 Rust 多得多”的刻板印象。但数据表明,虽然两者的“啰嗦”方式不同,但结果是一样的:

  • Go 的啰嗦:体现在显式的错误处理、显式的循环结构,以及为了简单性而不得不写的重复逻辑。
  • Rust 的啰嗦:体现在复杂的类型系统设置、Trait 的实现(impl blocks)、以及为了满足借用检查器而编写的“仪式性”代码。

正如作者所总结的:

Go 狂热者:“Go 很简单!” -> “是的,简单到你需要把同一件事写很多遍。”
Rust 狂热者:“Rust 完美表达!” -> “是的,但你花了 40% 的时间在写 setup 代码和 trait 实现。”

其他颠覆性的发现

除了 Go 和 Rust 的“握手言和”,这份报告还有几个极具冲击力的发现:

1. Lisp 家族是“干度之王”

Clojure 以 77.91% 的惊人密度位居榜首。Haskell 紧随其后。

这验证了一个古老的观点:如果你想要最高的“人类思想 vs 击键次数”比率,Lisp 和函数式语言依然是王者。它们几乎每一行代码都是纯粹的业务逻辑。

2. Java 居然比 Go 和 Rust 都“干”?

Java 的得分为 65.72%,显著高于 Go、Rust 和 C#。

这听起来反直觉,毕竟 Java 以 PublicStaticVoidMain 这种冗长著称。但这可能说明:

  • 现代 Java 及其生态(Spring 等)通过注解等方式极大地消除了样板代码。
  • 或者,Top 100 的 Java 项目多为成熟的业务系统,核心逻辑占比大,而 Go/Rust 项目中系统级代码(通常包含更多底层重复逻辑)较多。

3. 脚本语言的特异性

Shell Script 的密度极高(72.24%),但这主要是因为 Shell 脚本通常很短且高度定制化(Bespoke),很难复用,因此“唯一性”很高。

小结:复杂度的守恒

这个研究告诉我们一个道理:语言特性(Features)并不一定能消除复杂度,它往往只是转移了复杂度。

Go 选择了少量的特性,导致逻辑必须通过显式的重复代码来表达;Rust 选择了丰富的特性(宏、泛型、Trait),导致开发者必须编写大量的结构性代码来支撑这些特性。

对于 Gopher 来说,这或许是一种宽慰:别再为 if err != nil 感到羞愧了。隔壁写 Rust 的兄弟,虽然代码看起来很酷,但他们为了让编译器开心而敲击键盘的次数,并不比你少。

毕竟,软件工程没有银弹,只有取舍。

资料链接:https://boyter.org/posts/boilerplate-tax-ranking-popular-languages-by-density/


聊聊你的“啰嗦”体验

看完这个数据,你是感到“意料之中”还是“大吃一惊”?在你的实际开发中,你觉得 Go 的 if err != nil 更磨人,还是 Rust 的类型体操和 Trait 实现更让你头大?你认同“复杂度守恒”这个观点吗?

欢迎在评论区留下你的看法,让我们来一场理性的“语言之争”!


还在为“复制粘贴喂AI”而烦恼?我的新专栏 AI原生开发工作流实战 将带你:

  • 告别低效,重塑开发范式
  • 驾驭AI Agent(Claude Code),实现工作流自动化
  • 从“AI使用者”进化为规范驱动开发的“工作流指挥家”

扫描下方二维码,开启你的AI原生开发之旅。


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

  • 想写出更地道、更健壮的Go代码,却总在细节上踩坑?
  • 渴望提升软件设计能力,驾驭复杂Go项目却缺乏章法?
  • 想打造生产级的Go服务,却在工程化实践中屡屡受挫?

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

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

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


商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 AI原生开发工作流实战 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com
这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats