代码一团糟时,开发者会说它像意大利面。但究竟什么才是意大利面?
在预定为下一篇文章的 代码异味 #2中,我们继续了解下一个包。
因为太长?因为太乱?
都不是。意大利面指的是 控制流。
当你试图用眼睛跟踪执行顺序,却发现流程像面条一样纠缠,无法判断会跳到哪里,这才是它原本的含义。
将控制流和职责纠缠在同一个文件中的代码进行拆分,其过程可在 将关注点分离应用于 800 行组件的案例继续查看。
追溯这个词的根源,会找到一封发表于 1968 年的信。
迪杰斯特拉写的一封信
1968 年 3 月,埃兹赫尔·迪杰斯特拉在 ACM(Association for Computing Machinery,美国计算机协会)期刊上发表了一篇短文(原文)。
标题是《Go To Statement Considered Harmful》。正文是一封两页的信。
迪杰斯特拉最初拟定的标题是《A Case Against the Go To Statement》。如今这个挑衅性的标题,是当时的编辑尼克劳斯·维尔特改的。
这种标题格式后来固定成“○○ Considered Harmful”这一惯用表达,可见编辑的影响相当深远。
核心论点是:人们阅读静态代码,并在脑中构建动态执行过程。
但在包含大量 goto 的代码中,仅知道“当前正在执行这一行”无法解释程序处于什么状态,因为不知道它从哪里跳转而来。
只使用顺序执行、条件分支和循环的代码则不同。
可以像描述坐标一样说明执行位置,例如“外层循环第三次迭代,内层 if 的真分支”。
goto会摧毁这个坐标系。
这一论点也有理论依据。早两年的 1966 年,科拉多·贝姆和朱塞佩·雅各比尼证明,任何程序都可以只用顺序、选择和循环来表达(论文)。
也就是说,即使没有 goto,数学上也保证可以实现。
goto 画出的图
看看当时的代码是什么样,就能有所体会。
10 IF X > 100 THEN GOTO 70
20 IF X < 0 THEN GOTO 90
30 Y = X * 2
40 IF Y > 50 THEN GOTO 70
50 PRINT Y
60 GOTO 100
70 PRINT "TOO BIG"
80 GOTO 100
90 PRINT "NEGATIVE"
100 END
即使只有十行,也必须把流程画在纸上才能理解。每个 GOTO是一条线,这些线会在上下方向交叉。
增加到 500 行后会怎样,并不难想象。
1970 至 1980 年代的 BASIC 和早期 Fortran 代码确实如此,而意大利面就是对那种纠缠代码的称呼。
goto 消失了,但意大利面留下了
奇怪的事情发生了。现代代码中几乎没有 goto。
Swift 甚至完全没有它。在支持它的语言中,用途也基本缩小为返回资源清理位置的惯用写法。
然而,“意大利面代码”这个词反而用得更频繁了。
因为 goto只是症状之一,并不是原因。
真正的问题是读者无法预测流程,而现代代码还有很多其他方式会制造这种问题。
**嵌套地狱。**if 里面套 if,里面是闭包,闭包里又套 if。
缩进像箭头一样不断加深,因此被称为末日金字塔。它不像 goto那样跳来跳去,但同样难以追踪每种条件组合会到达哪一行。
**回调地狱。**用回调串联异步任务,会让执行顺序与代码顺序错位。
当从上到下阅读不再等于执行顺序时,坐标系再次崩溃。
// 无法按代码顺序阅读执行顺序
loadUser(id) { user in
loadProfile(user) { profile in
loadPosts(profile) { posts in
DispatchQueue.main.async {
self.render(posts) // 错误处理应该放在哪一层?
}
}
}
}
**全局状态。**如果任意函数都能随时修改某个变量,就必须翻遍整个项目,追踪它的值为什么变成这样。
这也是单例经常受到批评的原因。
**事件汤。**A 发送通知,B 接收后修改状态,观察该状态的 C 又发送通知。
每个片段都短小整洁,但整体流程没有记录在任何地方。
这是响应式代码中常见的形式,在某些方面比 goto更难追踪,因为跳转目标没有写在代码里。
可以用数字衡量吗
“这段代码像意大利面”是主观说法。因此,1976 年托马斯·麦凯布提出了圈复杂度(Cyclomatic Complexity)这一指标(论文)。
计算很简单:把代码的控制流画成图,然后计算有多少条独立路径。
实际工作中,通常用分支点数量加 1 进行近似。可以大致认为每个 if、for、while、case、&&和??增加 1。
一种常见约定是,数值超过 10 就该拆分函数了。麦凯布本人也把这个数字定义为合理的上限,而不是绝对标准。
实际上,一个只用 switch列出 20 种情况的函数,复杂度可能超过 20,但仍然容易阅读。
SwiftLint 的 cyclomatic_complexity规则会测量类似的数值。
不过 SwiftLint 只统计 if、guard、for、while、repeat、case和catch,不统计 &&或??。默认警告阈值是 10。
在项目中启用它,悄悄增长的函数总有一天会被发现。
克努特的反驳
如果把这个故事总结为“迪杰斯特拉赢了”,就只了解了一半。
1974 年,唐纳德·克努特在一篇题为《Structured Programming with go to Statements》的长篇论文中提出了反驳(论文)。
他的要点是,goto本身并不是邪恶的。在一次跳出嵌套循环等场景中,使用goto反而能让流程更清晰。
为了强行移除它而创建布尔标志变量,再叠加条件语句,反而会产生更糟糕的代码。
现代语言得出的结论接近折中方案:取消无限跳转,但为常见模式提供专用语法。
break、continue、带标签的 break label,以及 Swift 的 defer和guard都是例子。
// guard: 在上方处理例外情况,让主体保持扁平
func process(_ data: Data?) throws -> Packet {
guard let data else { throw ParseError.empty }
guard data.count > headerSize else { throw ParseError.tooShort }
// 从这里开始,所有条件都已处理完毕
return try decode(data)
}
guard类似于 C 时代的 goto cleanup模式最终成为语言功能。它把跳转目标固定到一个位置,并为其命名。
总结
- 意大利面代码不是脏乱的代码,而是无法预测控制流的代码。
- 起点是迪杰斯特拉 1968 年的信,核心论据是“应该能够用坐标描述执行位置”。
goto消失了,但深层嵌套、回调嵌套、全局状态和事件链会重新制造同样的问题。- 可以用圈复杂度进行粗略衡量,10 左右是常见的警告线。
- 正如克努特反驳的那样,目标不是消灭
goto,而是让流程具有可预测性。guard和async/await是由语言代替我们守护这一目标的机制。
下一篇将讨论朝相反方向损坏的代码。流程非常规整,但只要增加一个值,就必须修改七个文件。
这就是千层面代码。
延伸阅读
来源与验证
- Go To Statement Considered HarmfulEdsger W. Dijkstra Archive · 作者原文 · 核查 2026年8月17日依据: 1968 年对 goto 的批评与控制流推理问题
- Flow Diagrams, Turing Machines and LanguagesCorrado Böhm·Giuseppe Jacopini · 研究论文 · 核查 2026年8月17日依据: 1966 年顺序、选择与循环结构的表达能力
- A Complexity MeasureThomas J. McCabe · 研究论文 · 核查 2026年8月17日依据: 1976 年圈复杂度的定义与控制流图
- Structured Programming with go to StatementsDonald E. Knuth · 研究论文 · 核查 2026年8月17日依据: 1974 年结构化编程与 goto 的有限作用

![[代码异味 #1] 为什么意大利面代码是意大利面? 封面图](/assets/images/posts/a1c72f38-347f-4ee2-8e53-9a021038d44f/spaghetti-code-1.jpg)