程式碼一團亂時,開發者會說它像義大利麵。但究竟什麼才是義大利麵?
在預定為下一篇文章的 程式碼異味 #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 行後會怎樣,不難想像。
1970~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) // 錯誤處理應該放在哪一層?
}
}
}
}
**全域狀態。**如果有任何函式都能隨時修改的變數,就必須翻遍整個專案,追蹤它的值為何變成現在這樣。
這也是 singleton 經常受到批評的原因。
**事件湯。**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模式定型為語言功能。也就是固定跳躍目的地並替它命名。
總結
- 義大利麵程式碼不是髒亂的程式碼,而是無法預測控制流程的程式碼。
- 起點是 Dijkstra 1968 年的信,核心論據是「應該能用座標描述執行位置」。
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)