程式碼一團亂時,開發者會說它像義大利麵。但究竟什麼才是義大利麵?
在接下來的程式碼異味 #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反而能讓流程更清楚。
如果為了強行移除它而建立 Boolean 旗標變數,再堆上條件式,結果會變成更糟的程式碼。
現代語言的結論接近折衷方案:移除無限制跳躍,但為常見模式提供專用語法。
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)