代码一团糟时,开发者会说它像意大利面。但究竟什么才是意大利面?
在接下来的代码异味 #2中,我们继续了解下一个包。
因为很长?因为很乱?
都不是。意大利面指的是控制流。
它原本指执行顺序像面条一样纠缠,想用眼睛跟踪时,根本不知道下一步会跳到哪里。
将控制流和职责纠缠在同一个文件中的代码拆分过程,请继续阅读将关注点分离应用于 800 行组件的案例。
追溯这个说法的根源,会找到一封发表于 1968 年的信。
Dijkstra 写的一封信
1968 年 3 月,Edsger Dijkstra 在 ACM(Association for Computing Machinery,美国计算机学会)期刊上发表了一篇短文(原文)。
标题是《Go To Statement Considered Harmful》。正文是一封两页的信。
Dijkstra 原本给出的标题是《A Case Against the Go To Statement》。现在这个挑衅性的标题,是当时的编辑 Niklaus Wirth 改的。
这种标题格式后来固定成“○○ Considered Harmful”这一惯用语,可见编辑的影响延伸得相当远。
核心观点是:人们阅读静态代码,并在脑中构建动态执行过程。
但在goto很多的代码中,仅知道“现在正在执行这一行”并不能说明程序处于什么状态,因为不知道它是从哪里来的。
只使用顺序执行、条件分支和循环的代码则不同。
可以像描述坐标一样描述执行位置,例如:“外层循环第三次迭代,内层 if 的 true 分支”。
goto会破坏这个坐标系。
这个观点也有理论依据。两年前的 1966 年,Corrado Böhm 和 Giuseppe Jacopini 证明,任何程序都可以只用顺序、选择和循环来表达(论文)。
数学由此保证了即使没有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 行后会怎样,不难想象。
20 世纪 70~80 年代的 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 年 Thomas McCabe 提出了圈复杂度(Cyclomatic Complexity)指标(论文)。
计算很简单:把代码的控制流画成图,然后数有多少条独立路径。
实践中通常用分支点数量加 1 来近似。可以大致认为每个 if、for、while、case、&&和??增加 1。
通常认为数值超过 10 就该拆分函数了。McCabe 本人也将这个数字定义为合理上限,而不是绝对标准。
实际上,用一个switch列出 20 种情况的函数,复杂度会超过 20,但仍然容易阅读。
SwiftLint 的cyclomatic_complexity规则会测量类似的数值。
不过 SwiftLint 只计算if、guard、for、while、repeat、case和catch,不计算&&或??。默认警告阈值是 10。
在项目中启用后,那个悄悄变大的函数总有一天会被发现。
Knuth 的反驳
如果把这个故事总结成“Dijkstra 赢了”,就只了解了一半。
1974 年,Donald Knuth 发表了一篇名为《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 年 Dijkstra 的信,核心论据是“必须能够用坐标描述执行位置”。
goto消失了,但深层嵌套、回调嵌套、全局状态和事件链会重新造成同样的问题。- 可以用圈复杂度进行粗略衡量,10 左右是常见的警告线。
- 正如 Knuth 所反驳的,目标不是消灭
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)