跳轉到

1968 GOTO 有害論:一封信引發的軟體革命#

案發現場#

1960 年代末,電腦工業撞上了第一堵真正的牆。硬體按摩爾定律突飛猛進,但軟體的開發卻頻頻失控:IBM 的 OS/360 作業系統動員上千名程式設計師、花費數億美元,推出後仍 bug 疊出;NATO 於 1968 年在德國加米施(Garmisch)召開會議,首次正式使用了「軟體危機」(software crisis)一詞。專案延期、預算爆炸、品質崩壞——程式越寫越大,混亂卻呈指數成長。

混亂的元凶在哪裡?追查現場可以發現一個共同特徵:程式碼裡佈滿 GOTO。BASIC(見 1964-BASIC.md)與組合語言的程式,控制流像一團義大利麵:第 100 行跳到第 50 行,第 50 行跳到第 900 行,條件跳躍再跳回來。閱讀程式的人無法「從上到下」理解執行順序,任何修改都可能牽一髮動全身。

遇見這個問題的核心人物是荷蘭電腦科學家 Edsger W. Dijkstra——他正是 ALGOL 60 報告的實作先驅(第一個 ALGOL 60 編譯器的實作者)、堆疊式執行模型的提出者。1968 年 3 月,他在《Communications of the ACM》上發表了一封只有一頁多的信,標題是編輯 Bruce Gilmour 建議的 「Go To Statement Considered Harmful」(GOTO 敘述有害論)。這封信被公認為程式設計史上影響最大的單一出版物。

偵查過程#

線索一:追蹤程序的「動態過程」為何痛苦#

Dijkstra 的推理從一個認知科學式的觀察出發:人類心智善於靜態地理解空間關係,卻拙於在腦中追蹤動態的時間過程。他指出,程式設計師的智能力量主要用於「想像程式執行時狀態的變化」;而 GOTO 的罪惡不在語法層面,而在語義層面——它讓「程式的靜態文本」與「執行的動態過程」失去對應關係:

100 IF X > 0 THEN GOTO 300
200 LET Y = Y + 1
210 GOTO 500
300 LET Y = Y - 1
310 IF Y < 0 THEN GOTO 200
400 PRINT Y
410 GOTO 100
500 REM ...

讀這段程式,你必須在腦中同時追蹤多個跳躍目標與回傳點,執行順序無法從文本的幾何結構中讀出。Dijkstra 推導出的結論是:程式的品質與「靜態文本的可理解性」成正比,而 GOTO 正是摧毀這種對應的元凶。他寫下那句名言的核心:GOTO「把程式的執行過程變成一隻游標在頁面上亂竄,這種 unbridled use(不受節制地使用)將是災難」。

線索二:Böhm-Jacopini 定理——三種結構即可表達一切#

如果 GOTO 要被取消,拿什麼取代?答案是 1966 年義大利學者 Corrado Böhm 與 Giuseppe Jacopini 證明的結構化定理:任何單入口單出口(single-entry single-exit)的流程圖,都可以只用三種控制結構組合表達:

  1. 順序(sequence): \(S_1;\ S_2;\ \dots\ S_n\) ——一個接一個執行。
  2. 選擇(selection):if P then S1 else S2——二元分支。
  3. 循環(iteration):while P do S——條件重複。

用數學語言說:

\[ \text{任何可計算函數的流程圖} \;\subseteq\; \langle \text{順序} \rangle \;|\; \langle \text{選擇} \rangle \;|\; \langle \text{循環} \rangle \text{ 的組合} \]

這個定理的深意在於:它證明了 GOTO 在表達能力上是多餘的——取消 GOTO 不會喪失任何計算能力。Dijkstra 在信中引用了這個結果,把「GOTO 可否被取代」從哲學辯論變成數學定理。而結構化程式的關鍵美德是:三種結構都是單入口單出口,因此可以層層巢狀組合,程式成為一棵從上到下、可逐塊理解與驗證的樹。

線索三:從「證明正確」到「設計正確」#

Dijkstra 的推理還有一層:他相信程式應該能被數學證明正確。GOTO 滿天飛的程式,其正確性證明需要無窮多個跳躍案例;而結構化程式(如 while 迴圈)可以用循環不變式(loop invariant)歸納證明。這個推理催生了他後來的名著《A Discipline of Programming》(1976)與最弱前置條件(weakest precondition)理論。程式設計從「寫了再 debug」轉變為「設計時就證明正確」——這是形式化方法的濫觴。

結案報告#

這封信引爆了結構化程式設計運動,其影響延續至今:

  • 語言層面:1970 年代的語言紛紛捨棄或限制 GOTO——1970-Pascal.md 的 Pascal 幾乎不需要 GOTO;C 語言以 while/for/if-else 為主幹;Ada、Modula-2 全面結構化。連 BASIC 也在 1980 年代進化出無行號的 QBASIC 與結構化的 Visual Basic。
  • 方法論層面:Niklaus Wirth 的「逐步求精(stepwise refinement)」、David Parnas 的模組化資訊隱藏、乃至頂向下設計,都建立在結構化定理之上。軟體工程從此成為一門學科,NATO 的加米施會議正是其開端。
  • 教育層面:結構化程式設計成為 1970–80 年代大學入門課程的教條,「禁止使用 GOTO」寫進了無數課堂規則與公司程式碼規範。
  • 理論層面:Dijkstra 的論證風格催生了 Hoare 邏輯(1969,Hoare 為 Pascal 的前身 ALGOL W 建立的公理語義)、最弱前置條件、以及今日的形式化驗證工具(SPIN、TLA+)。
  • 餘音:GOTO 並未完全消失——C 的 goto 仍存在(用於錯誤處理跳出多層迴圈)、Linux 核心程式碼裡仍有節制地使用。Donald Knuth 1974 年的〈Structured Programming with go to Statements〉提出和解:GOTO 有其低階用途,但必須節制且跳躍方向向下。這場辯論至今偶爾重演,但主流共識已定:控制流應由結構而非跳躍組成。

一句話總結:這是程式設計史上最著名的「一頁紙偵查報告」——它用一封信、一個定理(Böhm-Jacopini),把 GOTO 釘上了歷史的恥辱柱,換來了結構化程式設計與現代軟體工程。

證據與工具#

以下用 Python 對照「GOTO 滿天飛」與「結構化」兩種寫法,並模擬 Böhm-Jacopini 定理:

# 案發現場:GOTO 版本(非結構化,模擬 BASIC 的行號跳躍)
program = {
    100: "IF X > 0 THEN GOTO 300",
    200: "LET Y = Y + 1",
    210: "GOTO 500",
    300: "LET Y = Y - 1",
    310: "IF Y < 0 THEN GOTO 200",
    400: "PRINT Y",
    410: "GOTO 100",
}

def goto_flow(prog, start=100):
    """模擬 GOTO 程式的控制流:靜態文本 vs 動態跳躍順序完全脫節"""
    visited, pc, trace = [], start, []
    while pc not in visited and pc in prog:
        visited.append(pc)
        trace.append(pc)
        stmt = prog[pc]
        if "GOTO" in stmt:
            pc = int(stmt.split("GOTO ")[1])     # 跳到任意行號
        else:
            keys = sorted(prog.keys())
            pc = keys[(keys.index(pc) + 1) % len(keys)] if keys.index(pc) + 1 < len(keys) else start
    return trace

print("GOTO 版動態執行軌跡(與文本順序脫節):", goto_flow(program))

# 結案報告:結構化版本(while/if-else,從上到下單一入口出口)
def structured_flow(x):
    y = 0
    while x > 0:          # 循環:條件重複
        y = y - 1
        if y < 0:          # 選擇:二元分支
            y = y + 1      # 順序:語句接續
        x = x - 1
    return y

print("結構化版執行結果:", structured_flow(5))

# Böhm-Jacopini 定理模擬:任意流程圖可用三種結構還原
def flowchart_to_structured(nodes, edges, entry):
    """把流程圖(節點+邊)轉換為結構化程式的骨架:
       順序(鏈)、選擇(分岔)、循環(回邊)"""
    result = []
    visited = set()

    def emit(n):
        if n in visited or n is None: return
        visited.add(n)
        node = nodes.get(n)
        outs = edges.get(n, [])
        if node is not None:
            result.append(("SEQ", node))            # 順序結構
        if len(outs) > 1:
            result.append(("IF-ELSE", outs))        # 選擇結構
            for o in outs: emit(o)
        elif outs and outs[0] < n:
            result.append(("WHILE-BACK", outs[0]))  # 循環結構(回邊)
        elif outs:
            emit(outs[0])

    emit(entry)
    return result

nodes = {1: "x > 0 ?", 2: "y = y - 1", 3: "y < 0 ?", 4: "x = x - 1", 5: "return y"}
edges = {1: [2, 5], 2: [3], 3: [4, 2], 4: [1], 5: []}
print("\n流程圖還原為三種結構:")
for kind, detail in flowchart_to_structured(nodes, edges, 1):
    print(f"  {kind}: {detail}")
print("\n結論:順序 + 選擇 + 循環,即可表達一切演算法——GOTO 在表達能力上是多餘的")

執行結果顯示三件事:第一,GOTO 版的動態軌跡(如 100→300→310→200→…)與文本幾何順序完全脫節——這正是 Dijkstra 指出的認知災難;第二,結構化版本只用 while 與 if-else 就表達了相同邏輯,從上到下單一入口出口;第三,流程圖還原器把任意的節點與邊分解為 SEQ、IF-ELSE、WHILE-BACK 三種結構——這就是 Böhm-Jacopini 定理的具體呈現:順序、選擇、循環即可表達一切演算法。