Java vs Node.js vs Go:寫慣 Node/Go 之後,重新學 Java 的優劣勢筆記
家人在學 Java,跟著學的過程一直忍不住拿它跟每天在用的 Node.js、Go 比較。並行模型(event loop vs goroutine vs Java 21 的 virtual thread)、部署與冷啟動、生態系成熟度,拿自己手上的專案(moon-bbq-backend、COME.ANC13、PTT bot)當實際案例對照,而不是空泛比較。
這篇不是課程系列的一部分,是學 Java 的另一個動機順便寫的——家人正在學 Java,我也跟著學,學的過程一直忍不住拿它跟自己每天在用的 Node.js(tommy-blog、COME.ANC13)跟 Go(moon-bbq-backend、Focus-Island)比較。整理成一篇筆記,老實記錄「同一件事,三個語言/生態系分別怎麼做」。
一、並行模型:這是三者差最多、也最該搞懂的地方
Node.js:單執行緒 + Event Loop
Node 的 JS 程式碼本身是單執行緒跑的,靠 event loop 處理並行——I/O(讀檔、打 API、查資料庫)丟出去之後不會卡住,等結果回來再排進佇列繼續執行。好處是不用處理執行緒安全這種問題(反正你的程式碼本來就不會有兩段同時在跑),代價是如果真的有 CPU 密集運算,會直接卡住整個 event loop,所有其他請求都要排隊等——這也是為什麼 Node 適合 I/O bound 的工作(API server、爬蟲),不適合影像處理、大量運算這類 CPU bound 的工作。
Go:Goroutine,語言內建的輕量並行
Go 的並行單位是 goroutine——不是 OS thread,是 Go runtime 自己管理的輕量執行單位,一個 OS thread 上可以同時排程成千上萬個 goroutine(M:N 排程模型)。go func() { ... }() 一行就能開一個新的並行執行流,搭配 channel 做 goroutine 之間的溝通。moon-bbq-backend 的 Hub 架構就是活生生的例子——每個 WebSocket 連線背後就是一個 goroutine,同時處理幾百個連線,記憶體/排程開銷小到幾乎感覺不到,這是 Go 在「大量並行連線」這類場景特別吃香的原因。
Java:傳統是重量級 Thread,但 Java 21 帶來了 Virtual Thread
這是最容易對 Java 有過時印象的地方。傳統 Java 的 Thread 直接對應一個 OS thread,開一個 thread 的成本不低(預設 stack size 通常是 512KB~1MB),開幾千個 thread 系統資源就會吃緊——這也是為什麼過去 Java 後端常常要靠 thread pool(例如 Tomcat 的 worker thread pool)限制同時處理的請求數,而不是來一個請求就開一個 thread。
但 Java 21(這次特地裝的版本)引入了 Virtual Thread(JEP 444,正式定案),概念上直接是衝著 Go 的 goroutine 設計哲學來的——虛擬執行緒由 JVM 自己排程,不是 1:1 對應 OS thread,可以輕鬆開出幾十萬個。以前寫 Java 並行程式,為了效能常常要用 callback 或 reactive(CompletableFuture、Reactor)把程式碼寫成非同步、非阻塞的樣子,可讀性通常比同步程式碼差很多;虛擬執行緒讓你可以繼續寫看起來完全同步、阻塞式的程式碼,底層卻有接近 goroutine 的並行效率——這算是 Java 生態這幾年最重要的一次補課。
| Node.js | Go | Java(傳統 Thread) | Java 21+(Virtual Thread) | |
|---|---|---|---|---|
| 並行單位 | 無(單執行緒 + event loop) | Goroutine(M:N 排程) | OS Thread(1:1) | Virtual Thread(M:N 排程) |
| 開銷 | 不適用 | 極低 | 較高(MB 級) | 低(接近 goroutine) |
| CPU 密集運算 | 會卡住 event loop | 可以搭配多核心 | 可以 | 可以 |
| 寫法心智負擔 | 要習慣 async/await、callback | 相對直覺(同步寫法 + goroutine) | 傳統寫法簡單但效能受限;要效能常要上 reactive,可讀性下降 | 同步寫法 + 高並行效率,兩者兼得 |
二、部署與執行環境:編譯產物差很多
這塊直接對照自己踩過的坑最有感。
Go 編譯成一個平台專屬的原生執行檔,沒有任何額外的執行環境依賴(除非用了 CGO)。moon-bbq-backend 的 Dockerfile 第二階段直接 FROM gcr.io/distroless/static-debian12,把編譯好的單一 binary 丟進去就結束,image 小到誇張,啟動幾乎是瞬間——這對 Render 這種免費/低資源方案特別友善,冷啟動的痛苦被壓到最低。
Node.js 不用編譯,但要帶著 node_modules 一起部署(除非另外打包),而且執行期一定要有 Node runtime——沒有 Node,.js 檔案什麼都不是。啟動速度通常也快,但比不上 Go 的原生執行檔。
Java 是三者裡部署考量最重的:一定要有 JVM 執行環境,.jar 檔案本身不是機器碼,一定要靠 java -jar 透過 JVM 跑。更麻煩的是 JVM 的啟動與 warm-up 時間——JIT 編譯器是在程式跑起來之後才逐步把熱點程式碼優化成原生機器碼,代表 Java 程式剛啟動的那幾秒到幾十秒效能是相對差的,要跑一段時間效能才會真正上來。這個特性在 serverless/Lambda 這種「常常要冷啟動」的場景特別痛——之前寫 PTT MacShop bot 那個 AWS Lambda 專案時完全沒考慮過 Java runtime,直接選 Node,冷啟動速度就是原因之一(AWS 自己的 Lambda 冷啟動效能比較資料裡,Java runtime 通常也是墊底那一批)。近幾年 Java 生態有 GraalVM Native Image 這類方案想解決這個問題(把 Java 直接編譯成原生執行檔,犧牲一些彈性換啟動速度),但這已經不是「寫普通 Java」的體驗了,是額外一整套工具鏈。
三、型別系統與寫起來的體感
Go 刻意走「簡單」路線——很長一段時間沒有泛型(直到 Go 1.18 才加入),錯誤處理用「多回傳值 + 手動檢查 if err != nil」而不是例外機制,語言本身的關鍵字數量刻意壓得很少。優點是任何人接手一份 Go 程式碼,學習曲線都很平——這也是為什麼團隊/開源協作場景很喜歡 Go,程式碼風格天生就很難寫得太花俏。缺點是有些場景(例如寫起來需要大量樣板的錯誤檢查)確實比較囉唆。
Node/TS 是光譜的另一端——JS 本身動態、彈性極高,TS 又是後來補上去的型別系統,能做到的表達力很強(泛型、聯合型別、條件型別),但也代表同一件事可以有很多種寫法,團隊風格如果沒有共識容易發散。
Java 語法本身偏「囉唆但明確」——每個東西的型別都要寫清楚,物件導向的一整套機制(class/interface/存取權限)從語言最初設計就很完整,不像 JS 的 class 是後來加上去、底層其實還是 prototype。這篇系列前幾篇筆記提到的「TS 的 interface 編譯完就消失,Java 的是執行期真實存在的契約」,就是這種「明確換取代價」的一個具體例子——多打的那些字,換來的是編譯器能幫你擋下更多錯誤,以及像 Spring 這種框架能利用執行期型別資訊做依賴注入這類事情。現代 Java(var 型別推斷、record、switch expression 這些新語法)有在往「減少樣板」的方向補,但整體語言哲學還是偏向明確、嚴謹。
四、生態系與框架成熟度
Spring 生態系的成熟度是三者裡最深的——依賴注入、AOP、資料庫存取(Spring Data JPA)、安全性(Spring Security)、雲原生(Spring Cloud)幾乎每個後端會遇到的問題都有官方或準官方的解法,而且彼此高度整合。代價是學習曲線陡、專案骨架相對厚重,一個 Spring Boot 專案的「約定跟魔法」比 Express/Gin 這類輕量框架多很多。
Node 生態系(npm)套件數量最龐大,幾乎什麼都找得到現成的,但品質、維護狀態參差不齊是常態,要自己花力氣篩選。
Go 生態系相對精簡,但標準函式庫本身就很夠用(net/http 直接可以寫一個堪用的 HTTP server,不像 JS 生態高度依賴第三方框架),第三方套件生態沒有 npm 或 Maven Central 那麼龐大,但常用的那幾個(Gin、GORM、Redis client)都很成熟穩定。
五、老實說:新專案會怎麼選
拿自己手上的專案對照,其實已經誠實反映了這幾個語言的取捨:
- moon-bbq-backend 選 Go:即時 WebSocket、大量並行連線、要部署在資源有限的免費方案(Render 0.1 CPU/512MB)——goroutine 的並行效率 + 原生編譯的小 image/快啟動,剛好對上這個場景的每一個需求。
- tommy-blog、COME.ANC13 選 Next.js/Node:前後端同一個語言、開發速度優先、團隊(或未來團隊)找 JS/TS 工程師的市場supply最大,不是效能敏感的場景。
- PTT MacShop bot 選 Node 寫 Lambda,不是 Java:serverless 場景,冷啟動速度是硬指標,Java/JVM 的 warm-up 特性剛好是最大扣分項。
那 Java 適合什麼場景?誠實講,以自己現在的專案規模跟場景,還真的想不到一個「非 Java 不可」的理由——但這反而印證了語言選擇的重點:Java 真正的主場是大型企業後端、金融/保險這類對穩定性、長期維護、既有人才庫深度要求很高的場景,Spring 生態系的成熟度、JVM 幾十年累積下來的效能調校經驗跟監控工具鏈,是這類場景真正在乎的東西,不是「哪個語言 Hello World 寫起來比較潮」。這也是為什麼即使 Node/Go 在新創、雲原生場景很流行,Java 在企業 IT 市場的職缺量跟穩定性依然很難被取代——這次學 Java,對這句話有更具體的體感,不只是聽業界說法而已。