Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
【譯】Rust vs. Go

【譯】Rust vs. Go

本文譯自 Julio MerinoRust vs. Go。Julio Merino 目前是 G 社僱員,在 G 社工作超過 8 年,無論工作內外,都接觸開發不少 Go 語言,並撰寫 Rust 點評系列文,來聽聽他對 Rust 與 Go 的想法吧。

Thanks Julio Merino for this awesome article!


歡迎來到「Rust 點評」系列特別篇,也是我在系列文開始就承諾撰寫的主題,將探討一個難以忽視的大哉問:Rust 與 Go 孰優孰劣?

這麼比較並沒有根據,所以不會有標準答案。我認為人們會把這兩種語言作伙比較只因為它們幾乎同時釋出,而且 Rust 的釋出像是在回應 Go。除此之外,兩種語言都被認為聚焦在系統軟體上(system software),但其實它們大相徑庭,就算都專注系統軟體,各自目標的軟體類型也不盡相同。

Go 可以視為「做對了的 C」或是「Python 的替代品」。Go 在開發網路伺服器與自動化工具的領域發光發熱。Rust 專注在正確與安全性,定位在 C++ 與 Haskell 之間,如同之前提及,可以視為「務實的 Haskell」。儘管 Rust 的語言抽象程度很高,它仍承諾這些抽象是零成本(zero-cost abstraction),也就是說,它應該擅長寫任何系統專案。

這篇個人點評基於我用兩種語言寫了相同的專案 sandboxfs。最初實作是用 Go,而我開發了另一個用 Rust 的實驗性改寫(還沒有完全檢驗),兩個實作都通過相同的測試套件(test suite)。除了透過這次改寫來學習語言,也因為當我分析 Go 實作版本的效能時,發現熱點總是在 Go 的執行環境(runtime),我想要嘗試看看簡單的 Rust 改寫後效能能否長進,而情況似乎就是如此。隨著這次改寫,我很訝異原本的 Go 實作版本有不少潛在的並行(concurrency)漏洞,因為許多 Rust 並不允許我利用相同的設計改寫。

在開始之前,也許你會想讀讀兩年前我對 Go 語言的點評。讀完了嗎?我們開始吧!

記憶體管理

要說 Rust 與 Go 之間最明顯的差異,首推各自的記憶體管理方式。

Go 根據物件的生命週期,決定物件要配置在 stack 或 heap 上,而後者會以垃圾回收機制(garbage collection)管理。Rust 則是手動(explicit)配置在 stack 和 heap 上,而後者會透過 scopes(C++ 用語中稱為 RAII)與 ownership/move semantic 管理。

在這個領域中,典型的權衡是垃圾回收與手動記憶體管理(explicit memory management)。這代表 Go 因為垃圾回收機制帶來額外開銷,而 Rust 並不會。但對大多數軟體而言不會是個問題。試想你上一次寫效能需求高,不能有延遲,且 CPU-bound 的程式是什麼時候了。

若要高效使用 Rust,需要瞭解電腦記憶體如何運作。Go 可以赦免你不懂記憶體管理,但如同我下一節將提到的,不懂記憶體並非好事:瞭解記憶體運作是通往程式設計大師之路的重要一步。

獲勝者:無法決定。Go 與 Rust 兩者都提供更不容易記憶體洩漏(memory leak)且具有優良人因設計(ergonomic)的記憶體管理機制。

難度

Go 是一個簡單中不失自身優雅的語言。Go 極度容易幾刻鐘內上手並搞出一堆程式碼。然而,如同我們在「Rust 點評:學習曲線」一文所述,這可能是謬誤:在你剛學了任何語言後馬上寫的程式碼都不會最符合該語言特性,最佳實踐的程式需要花更多時間熬出。

對 Rust 來說,不能否認它是一個複雜的語言,也許沒有 C++ 這麼複雜,但仍有大量的概念在其中。撰寫 Rust 程式的確需要更多精力。對應的收益是,當你的程式寫完且成功編譯,很高的機率它會安然無恙的運作。這情境無法完全套用在 Go 上,我見過太多太多執行期(runtime)錯誤崩潰了。

你要選擇自己的戰爭:快速地撰寫 Go,再花費額外的時間寫瑣細的測試與修正執行期的問題;或是花時間一步步撰寫穩健的 Rust 程式,避免構建後的問題。

獲勝者:難以決定。我們可以很簡單地說 Go 贏了,但我並不想要這樣定論,因為我個人喜歡花更多時間打造一個禁得起時間考驗的程式,而非在未來還必須追蹤複雜的記憶體與線程問題。

泛型

Go 不支援泛型。Go 的作者們並沒有堅決反對泛型,但他們聲稱無法簡潔地實作或支援,而開始動手做之前,必須先找到完美解法。因此,人們濫用 interface {},將之作為泛型使用,其他 Go 程式碼也無法受惠於以此實作的原生函式(如 append)。

Rust 剛好具有如你所願的泛型。這個泛型支援普通型別及 traits,而且你還可以透過 impldyn 這兩個新特性(從 2018 開始),控制 Rust 從 traits 上面產生的泛型機器碼(machine code)。

獲勝者:不用多說了,非 Rust 莫屬。

程式碼完備性(Code sanity)

我最不能接受 Go 語言缺失的功能是沒有提供零抽象成本的方法,透過程式碼自身來編撰穩健的程式。沒錯,自動程式碼格式化和強力建議程式如何寫這兩件事很美好,但這不足以強制改善程式邏輯。我發現自己常常撰寫過長的註解來解釋一些完全不可能的條件,或為什麼特定參數可以運作,還有變數與互斥鎖之間有什麼關係⋯⋯,這些都是未來程式會崩毀的病徵:註解無法透過編譯器檢驗完備性,而且註解一定會過時。只要有機會,所有東西都該以程式碼表達。

那麼,什麼我在 Rust 發現什麼 Go 缺少的功能,可令程式碼更穩健更能防範未然?

  • 斷言(Assertions)。斷言之所以無價,是因為它讓程式設計師們得以溝通不一定明顯的意圖。Rust 有斷言,反觀 Go 沒有。呃⋯⋯Go 的確有 panic 可以來模擬斷言,但這作法不被認可,你應該永不使用它。

    為什麼 Go 沒有斷言呢?以下是我瞎猜的:因為程式設計師常常誤用斷言來驗證使用者的輸入,這樣導致有不對或惡意輸入時產生執行期錯誤。於是,崇尚務實主義的 Go 要求把所有問題當成可以被控制的錯誤,從而避免「誤用斷言」這種情況。(嗯⋯⋯,這我過些時間想再寫一篇獨立的文章來討論⋯⋯)

    順便提個我常常聽到的說法:你可以從寫了多少斷言來判斷一個程式設計師的經驗是否老到,而越多代表越老練。歡迎對這個想法有任何評論。

  • 標註(Annotations)。有時候該用到的輸入參數沒使用到,或該檢查的回傳值沒檢查。或其他時候,你知道一個特定函式永遠不會返回,而且希望給予呼叫端這項資訊,讓編譯器閉嘴。舉個例子,缺少 return 陳述句。Go 沒有這些標註,讓程式設計是很難清楚表達意圖。而 Rust 嘟嘟好有這些標註。

    更糟的是,Go 有些特定內部函式的行為如 panic,這些編譯器知道無法回傳,但不可能在你手寫的 Go 程式碼中表達這件事。如果考慮這問題和前面提及泛型是同個缺點,再次提及比較有點失允。

  • 註釋即文件(Docstrings)。是的,Go 有 docstrings,但神基本。雖然大部分可以正常運作,但寫了一堆 Java 之後,知道事先定義好結構的 docstrings 能提供不少價值。許多工具可協助檢驗文件的完備性。舉例來說,IntelliJ 可以驗證參數名稱是否對應真正的函式參數,並且交叉參照其他類別是否也合法。

    Rust 的 docstrings 支援 Markdown 而比 Go 來得好些,但仍無明確的撰寫指引:似乎沒有一種標準方式撰寫,也不支援替每一項目個別撰寫文件,工具更無法交叉檢查 docstrings 是否和程式碼吻合。

  • 錯誤檢查(Error checking)。我是少數(?)喜愛 Go 手動錯誤傳遞的一分子。你說得沒錯,寫一堆錯誤檢查很惱人,但這樣做迫使你使用與其他語言不同的方式思考錯誤這檔事。

    很不幸,Go 選擇的寫法有些問題:一個函式總是回傳一個結果值與一個錯誤,呼叫端可以決定先檢查錯誤,再檢查值。語言本身並無強制做這件事,而我看多太多太多錯誤是在檢查錯誤前先取值所導致。另一方面,Rust 帶著更高端的型別封裝了值或錯誤,加上沒有 null 這個特點,這代表了呼叫端永遠不會在錯誤存在時取得結果值,反之亦然。可以參考看看 Result 型別還有我寫有關 match 關鍵字的文章。

讓我下個結論,一個正向的聲明,這兩個語言都不提供自動型別。舉例來說,Rust 與 Go 強制程式設計師們整數轉型至不同大小,於是任何可能溢位(overflow/underflow)之處顯露無遺。提醒你一下:Go 在這方面比 Rust 稍強些,因為 Go 的型別別名(type alias)語義上被視為不同的型別,編譯時需要手動轉型,而 Rust 則是當作語法上的別名(就像 C typedef 所作所為)。

獲勝者:Rust 輕鬆勝出。你可以主張這只是對 Go 缺乏功能的一些抱怨,而且應該接受 Go 就是這種風格,但我不行:這些抱怨就是討論上述失能讓 Go 無法寫出防範未然,晶瑩剔透的程式碼。

效能分析

Go 的簡便無所不在,連讓程式最佳化的工具也不例外。從效能分析(profiling)來看,Go 內建追蹤 CPU 和記憶體用量的機制,且能與 pprof 工具整合。可以很容易檢查 Go 程式並取得有用的資料來最佳化。

我還沒發現任何 Rust 的分析工具可以和 Go 的工具一樣整合 pprof。當然,有一個函式庫可以生成類似 pprof 的追蹤資料,但我無法簡易上手,安裝上也有點詭異(需要 gperftools 以顯示在系統上)。這篇舊文有相關資訊和工具可供參考。

獲勝者:就我目前所知,Go 在這方面大勝。

構建速度

打從娘胎開始,Go 就被設計為盡可能快速構建。就我所知,這是為了減少 Google 內部大型應用程式構建時間而做的嘗試。我猜測這合理解釋為什麼 Go 選擇 duck typing 來避免組件間強耦合,進而加速增量編譯(incremental compilation)。當我第一次在 NetBSD 自我建立(bootstrap)Go 整個工具鏈時,我非常震驚,整個流程只需要幾分鐘。我以前使用 clang 需要幾小時建立環境。

Rust 是家喻戶曉的編譯緩慢,所有完備性檢驗(sanity check),例如 borrow checker 和泛型,並不是白吃的午餐。我聽說有機會可以改善,但第一,我還沒研究過;第二,這說法尚未實現。

獲勝者:簡單啦,Go。

構建系統

依照現代程式語言的慣例,Go 和 Rust 都有自己的套件管理與套件相依追蹤工具。

在 Go 語言,有窮極簡單的 Go 內建工具允許我們取得套件與它的相依套件,且無需任何設定檔就可構建整個專案。聽起來超誘人的,但事實上略違反基本工程慣例,有點危險。舉例來說,Go 的工具總是從網站如 GitHub 取得最新快照版本的相依套件,但完全沒有任何機制指定版本,也無法確保惡意程式碼混入。我認為這應是受 Google 超大單一儲存庫(monorepo)如何運作,以及「在最新版構建」(build at head)的哲學影響,但這不太符合開源社群生態的期待。顯然 Go 社群最後接受了他們需要一個更好的解決方案,有許多提案也嘗試改善這種狀況。

在 Rust 這邊,我們有 Cargo,在專案中使用 Cargo 會需要比 Go 內建機制多一點點設定,但就只有一點點:典型的 Cargo.toml 只需列出幾行相依套件和該專案基本資料(metadata)即可。Rust 社群使用 Cargo 可以解析的語意化版號管理相依。換言之,Rust 和 Cargo 設計上就支援他們身處的生態系,而非趕鴨子上架。Cargo 完全是 Rust 最美好的功能之一。

有趣的是,人們可以透過 Bazel 構建 Go 與 Rust 的程式。這種情況下,Bazel 透過一系列條件(rules_go)修正了上述提及 Go 的已知問題:這些條件允許相依套件以及工具鏈固定版號。至於 Bazel 對 Rust 的支援還很陽春,對應的條件(rules_rust)並沒有太多功能。

獲勝者:只考慮原生構建系統,Rust 的 Cargo 工具鏈勝出。如果把 Bazel 拉進來討論,目前是 Go 略勝一籌。

單元測試

對任何程式碼來說,自動化測試是確保程式依照期望執行,並保證程式成長時仍能正確執行(例如:沒有寫測試就無法做大規模的重構)。Go 這方面的表現令我嗤之以鼻,我想我會保留內容給另一篇專文討論。Go 的 testing 套件看來完全不鳥現代的測試技術,一心走自己認定的套路。那欸安捏?捨棄斷言(assertion)不用,導致只有測試函式自己可以使測試失敗,不允許其他輔助函式或固定資料(fixture)。對不複雜的單元測試來說挺誘人的,但往往不好控制。最終,更複雜的測試讓真正的待測邏輯變得更隱晦,更難以理解。

謝天謝地,Go 有個第三方函式庫 testify 提供類 JUnit 的測試框架。這函式庫使用合理的語義進行測試。較嚴重的問題是 Go 社群文化較武斷,你可能沒有機會在專案中使用它。

另一頭,Rust 的測試函式庫較貼近你預期的其他語言的行為。換言之,你有斷言可以用。我發現比較奇怪的點是,Rust 推薦你把測試寫在與待測原始碼同一份檔案中。由於我還沒寫足夠的測試,不清楚在真實世界中這種做法到底會如何?

獲勝者:我很想說是 Rust,因為 Go 的測試途徑太令我失望了,但 Go 有 testify 套件存在,我必須說兩者平分秋色。

結論

每一種程式語言皆有優缺點,這些小小的權衡抉擇是我藉由 Go 與 Rust 做比較來 證明這個概念。在每個部分勝出的語言都不一樣,有些時候你根本無法決定誰更有優勢。

您身為工程師的職責就是瞭解每個專案該如何權衡,並替專案選擇最佳的工具。Rust 與 Go 只是工具,挑一個最適配團隊,專案,還有你自己吧(按照這個順序)。

如果你希望我能更具體,我會強力推薦在座的各位學習 Rust,與 borrow checker 與 ownership 和平共處。即使最終你並沒有使用 Rust,過程中所學都會回饋到其他語言上,連 Go 也不例外。


以上翻譯經過原作者同意,但譯者並非科班也沒有翻譯學位,若有語句不通順或不合理之處,請留言或與我聯繫,切莫直接怪罪原作者。

@Paulswith

This comment has been minimized.

Copy link

Paulswith commented Jun 28, 2019

翻译的很好了,非常感谢!

@haifengwang

This comment has been minimized.

Copy link

haifengwang commented Aug 16, 2019

个人兴趣学习,为了将来工作,还是得看生态。目前 go 的生态好于 Rust

@709248556

This comment has been minimized.

Copy link

709248556 commented Sep 6, 2019

个人兴趣学习,为了将来工作,还是得看生态。目前 go 的生态好于 Rust

java生态更好

@xiaoyue26

This comment has been minimized.

Copy link

xiaoyue26 commented Sep 22, 2019

翻译的很好了,非常感谢!

好委婉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.