1993 年,第一個動態網頁是這樣運作的:

HTTP 請求進來 → 伺服器啟動一個新的 Perl 行程 → 行程執行完、輸出 HTML → 行程死掉

每一個請求,都新建一個作業系統行程。每個行程要載入 Perl 直譯器、讀環境變數、初始化——大概 50-200ms。1993 年,這沒問題,每天幾千次請求。

到了 1997 年,Amazon 的日流量開始以百萬計。每個請求都啟動一個行程——伺服器直接被壓垮。

這就是 CGI 的極限,也是後端語言演進的第一個驅動力。


第一代:CGI 與 Perl(1993–1998)

解決了什麼:讓靜態 HTML 能夠動態產生。

撞到什麼牆:每請求一個 OS 行程,連線數一上去就崩。Perl 程式碼結構鬆散,10 個人寫 10 個風格,100 個頁面之後沒有人看得懂。

直接結果:你需要一種語言,能夠把「程式邏輯嵌進 HTML」,同時讓伺服器重用行程而不是每次重建。


第二代:PHP 與 Java Servlet(1998–2005)

PHP 解決了 CGI 的「每請求啟動行程」問題——mod_php 讓 PHP 直譯器住在 Apache 行程裡,請求來了直接執行,沒有行程啟動成本。順帶解決了「模板要怎麼寫」的問題,PHP 本身就是一個嵌進 HTML 的語言。

<?php
$user = $_GET['name'];
echo "Hello, $user";
?>

WordPress、Drupal、Facebook 早期版本——PHP 讓這一代的 web 開發門檻從「你要知道 Perl」變成「你會複製貼上就能跑」。

Java Servlet 走另一條路:強型別、物件導向、跑在 JVM 上,一個行程服務所有請求(thread pool)。金融業、企業系統選 Java,因為你不能讓伺服器「跑到一半不知道為什麼就炸了」。

PHP 撞到的牆:全局變數、register_globals、沒有命名空間,大型專案維護成本爆炸。Facebook 到最後不得不發明一個工具把 PHP 編譯成 C++(HHVM)。

Java Servlet 撞到的牆:XML 地獄。一個簡單的功能需要 web.xml、applicationContext.xml、hibernate.cfg.xml,三個 XML 配置才能跑起來。「為什麼我要寫這麼多 boilerplate?」


第三代:Rails 與 Django(2005–2010)

Ruby on Rails 在 2005 年做了一件事,讓所有人都很震驚:

rails new blog
rails generate scaffold Post title:string body:text
rails db:migrate
rails server

你有一個能 CRUD 的 blog 系統了。這在 Java 要花一週。

Rails 的核心主張是 convention over configuration——不需要配置,因為框架已經替你做了合理的假設(Model 放這、View 放那、URL 這樣命名)。Django 同時期在 Python 世界做了同樣的事。

解決了什麼:Java 的 boilerplate 地獄、PHP 的結構混亂。ORM、migration、MVC 分離——這一代把「架構最佳實踐」內建進框架。

撞到的牆:Rails 的 convention 同時是它的枷鎖。你的業務需求如果和 Rails 的假設不一樣,你每一步都在跟框架搏鬥。Ruby MRI 沒有真正的並行(GIL),高並行時橫向擴展靠的是多行程,記憶體吃很兇。Airbnb 用 Rails,最後一部分服務不得不遷到 Java/Go。


第四代:Node.js(2009–2015)

Ryan Dahl 在 2009 年做了一個觀察:

Web server 的大部分時間都在等 I/O——等資料庫回應、等檔案讀完、等第三方 API。等待的時候,OS thread 佔著記憶體什麼都沒做。

Node.js 的解答:單 thread + event loop。不等 I/O,把 callback 排進隊列,等 I/O 完成再繼續。一個行程,處理數千個並行連線,記憶體消耗遠低於多 thread 模型。

C10K 問題(一台伺服器撐一萬個並行連線)在 Node.js 出來之後第一次有了真正可用的解法。

同時,前端工程師可以用 JavaScript 寫後端了——npm 生態爆炸,lodashexpresssocket.io 這些套件幾週之內就有幾百萬下載。

撞到的牆

  1. Callback hell——非同步邏輯用 callback 寫到三層就讀不懂。2015 年 Promise 和 async/await 解了語法問題,但底層複雜度還在。
  2. CPU 密集任務直接阻塞 event loop——Node.js 的單 thread 架構,一個 while(true) 卡住,整台伺服器就沒有回應了。圖片處理、加密、PDF 生成——這些不能放進 Node.js 的 event loop。
  3. 弱型別帶來的維護地獄——undefined is not a function 在 runtime 爆,不在編譯時告訴你。

第五代:Go 與靜態型別的回歸(2012–)

Go 在 2012 年正式開始被廣泛採用,帶著幾個明確的主張:

解決了什麼

  • 並行不應該靠 callback:Goroutine 讓並行的寫法和同步程式碼一樣直接。go func() 啟動一個協程,成本比 OS thread 低三個數量級。
  • 靜態型別 + 編譯時錯誤:你在 go build 就知道型別錯了,不是在 production runtime。
  • 部署極簡:Go 編譯成單一 binary,不需要 JVM、不需要 Ruby runtime、不需要 node_modules——scp binary 到伺服器就跑。

Docker、Kubernetes、Prometheus、Terraform——這些工具選 Go,不是因為它流行,是因為它的 binary 部署和低記憶體佔用正好符合基礎設施工具的需求。

撞到的牆

  • 錯誤處理冗長:if err != nil 反覆出現,泛型 1.18 之前表達能力有限。
  • 生態系年輕,某些領域(ML、科學計算、金融衍生品)沒有成熟套件,選 Go 就是自己造輪子。

第六代:Rust(2015–)、Bun / Deno(2022–)

Rust 不是解決 Go 的問題,是解決一個更老的問題:有沒有辦法做到 C/C++ 的效能,但不靠手動記憶體管理?Borrow checker 在編譯時保證記憶體安全,沒有 GC,沒有 runtime overhead。Rust 1.0 在 2015 年發布,2019 年後隨著 WebAssembly 和 Tokio(async runtime)的成熟,開始被後端工程師認真考慮。

Rust 的代價是學習曲線——初學者花在跟 borrow checker 搏鬥的時間,比學語言本身還長。不是每個後端服務都需要 C 的效能,Rust 在系統程式設計和效能關鍵路徑上有位置,但不會取代 Go 或 Node 在業務邏輯層的位置。

Bun / Deno 是對 Node.js 本身的重寫——保留 JavaScript/TypeScript 生態,但解決 Node.js 自己製造的技術債(CommonJS vs ESM、node_modules 體積、原生 TypeScript 支援缺失)。Deno 1.0 是 2020 年,Bun 1.0 是 2023 年,這一代目前還在確認自己的位置。


看到了什麼模式?

每一代後端語言的崛起,都有一個具體的「上一代撞牆」:

語言 / 框架上一代的牆這一代的解法
PHP / JavaCGI 每請求一行程行程常駐 / Thread pool
Rails / DjangoXML boilerplate 地獄Convention over configuration
Node.jsThread 等 I/O 浪費資源Event loop + 非同步 I/O
GoCallback hell / 弱型別 / 部署複雜Goroutine + 靜態型別 + 單 binary
RustGC 帶來的 latency / 記憶體安全Borrow checker + 零成本抽象

這個模式有一個推論:當你學一個新語言,值得先問「它在解什麼上一代撞牆的問題?」。知道這個,你就知道它的設計決策背後的邏輯,也就知道它自己會在哪裡撞牆。

下一篇:並行模型演進