一位 Rust 開發者的 Go 初體驗

TiDB_Robot發表於2020-03-09

作者介紹:
Nick Cameron,PingCAP 研發工程師,Rust 語言核心成員。
感謝 Rust 語言中文社群夥伴們的翻譯和審校:
翻譯:尚卓燃
審校:吳聰、張漢東

過去幾周,我一直在用 Go 語言編寫程式。這是我首次在大型且重要的專案中使用 Go。在研究 Rust 的特性時,我也看了很多關於 Go 的內容,包括體驗示例和編寫玩具程式。但真正用它程式設計又是一種完全不同的體驗。

我覺得把這次體驗寫下來應該會很有趣。在這篇文章中,我會盡量避免將 Go 與 Rust 進行過多的比較,不過,由於我是從 Rust 轉向 Go,難免也會包含一些比較。應該事先宣告的是,我更偏袒 Rust ,但會盡力做到客觀。

總體印象

用 Go 程式設計的感覺很棒。庫程式裡有我想要的一切,總體實現較為完善。學習體驗也十分順暢,不得不說,Go 是一種經過精心設計的實用性語言。舉個例子:一旦你知悉了 Go 的語法,就能將其他語言中慣用法延續到 Go 中。只要你學會一些 Go,就可以相對輕易地推測 Go 語言的其他特性。憑藉一些來自其他語言的知識,我能夠閱讀並理解 Go 程式碼,而不需要過多的搜尋(Google)。

與 C/C++、Java、Python 等相比,Go 並沒有那麼多痛點,而且更具生產力。然而,它還是與這些語言處在同一個時代。儘管它從其他語言身上吸取了一些教訓,甚至我個人認為它可能是那一代語言中最好的那個,但絕對還屬於那一代語言。這是一種漸進式的改進,而不是推陳出新(需要明確的是,這不是意味著對其價值的批判,從軟體工程的角度,漸進式改進通常會帶來好的影響)。一個很好的例證是 nil:像 Rust 和 Swift 這樣的語言已經去除了 null 的概念,並且消除了相關的一整類錯誤。Go 降低了一部分風險:沒有空值(no null values),在 nil0 之間進行區分。但其核心思想仍未改變,同樣還會出現解空指標引用這種常見的執行時錯誤。

易學性

Go 非常易學。我知道人們經常吹捧這一點,但是我真的為自己生產力的飛速提高而感到震驚。多虧了 Go 語言以及它的文件和工具,我僅僅花了兩天時間就可以寫出「有價值」、可以提交的程式碼。

有助於易學性的幾個因素是:

  • Go 很精簡。很多語言都試圖讓自己看起來小巧,但 Go 真正做到了這一點(這基本上是一件好事,我對這種自律精神印象深刻)。

  • 標準庫很出色(同樣,也很小)。從生態系統中尋找並使用庫程式非常容易。

  • 幾乎沒有其他語言中所不具備的東西。Go 從其他既存語言中提取了很多內容,並進行完善,最後將它們很好地組合在一起。它在避免標新立異這一方面做了極大努力。

乏味的樣板式程式碼

Go 程式碼很快就會變得非常重複。這是由於它缺乏巨集或者泛型這種用於減少重複的機制(介面雖然有利於抽象,但在減少程式碼重複方面作用沒有那麼大)。最終我會寫很多函式,而他們除了型別不同之外其他甚至完全一樣。

錯誤處理也會導致重複。許多函式中像 if err != nil { return err } 這樣的樣板式程式碼甚至比那些真正有價值的程式碼還要多。

使用泛型或巨集來減少樣板式程式碼有時會受到批評,理由是不應為使程式碼易於編寫而使其喪失可讀性。我發現 Go 恰恰提供了一個反例,複製和貼上程式碼往往既快速又簡單,閱讀程式碼卻會令人灰心喪氣,因為你不得不忽略大量的無關程式碼或者在大量的相同程式碼中找到細微的不同。

我喜歡的東西

  • 編譯時間:絕對快,可以確定要比 Rust 快得多。但實際上,它並沒有我預期的那麼快(對於中型到大型專案,我感覺它的速度只是與 C/C++ 相接近,或者稍微快一點。而我更加期待能夠即時編譯)。

  • 協程(goroutine)和通道(channel):值得稱讚的是,Go 為生成協程和使用通道提供了輕量級的語法。儘管只是一個小細節,卻使 Go 的併發程式設計體驗比其他語言更優越,它真正揭示了語法的力量。

  • 介面:它們並不複雜,但是很容易理解和使用,並且在很多地方都很實用。

  • if ...; ... { } 語法:可以將變數的作用域限制在 if 語句真的很好。這與 Swift 及 Rust 中的 if let 起著相似的效果,但用途更為廣泛(Go 沒有像 Swift 和 Rust 那樣的模式匹配,所以它無法使用 if let )。

  • 測試和文件註釋都很容易使用。

  • Go 工具鏈非常友好:將所有東西都放在一個地方,而不需要在命令列上使用多個工具。

  • 擁有垃圾收集器(GC):不用考慮記憶體管理真的會使程式設計更加輕鬆。

  • 可變引數。

我不喜歡的東西

以下內容沒有特定的順序。

  • nil 切片:要知道 nilnil 切片和空切片三者都不相同,我敢保證我們只需要其中的兩個,而不需要第三個。

  • 列舉型別並不是第一公民:使用常量模擬列舉讓人感覺是一種倒退。

  • 不允許迴圈引用:這實際上限制了包在劃分專案模組中的可用性,因為它變相鼓勵了在一個包中堆積大量檔案(或擁有大量零碎的小包,如果本該放在一起的檔案四處分散,這也同樣糟糕)。

  • switch 允許出現遺漏匹配的情況

  • for ... range 語句會返回一對「索引/值」。要想只獲取索引很容易(忽略值就好);但若要只獲取值,則需要顯式宣告。在我看來,這種做法更應該顛倒過來,因為在大多數情況下,我更需要值而不是索引。

  • 語法:

    • 定義與用途存在不一致。
    • 編譯器有時會很挑剔(例如,要求或禁止尾隨逗號);通過良好的工具可以緩解這種困擾,但是有時仍然會產生一些惱人的額外步驟。
    • 使用多值返回型別時,型別上需要括號,但 return 語句中卻不需要。
    • 宣告一個結構體需要兩個關鍵字(typestruct)。
    • 採用大寫命名法來標記公共或私有變數,看起來就像匈牙利命名法那樣,但更糟糕。
  • 隱式介面。我知道它也出現在我喜歡的東西中,但有時候它確實很惹人煩——特別是當你試圖找出所有實現該介面的型別,或者哪些介面是為給定型別而實現的時候。

  • 你無法在不同的包中編寫帶有接收器的函式,所以即使介面是「鴨子型別」的,你也不能為其他包中的型別實現這個介面,這使得它們的用處大大降低。

還有我之前已經提過的,Go 缺少泛型和巨集

一致性

作為一名語言設計者和程式設計師,Go 最讓我驚訝的地方也許是它的內建功能和使用者可用功能之間頻頻出現不一致。許多語言的目標之一就是儘可能消除編譯器魔法,讓使用者也能使用內建功能。運算子過載是一個簡單但有爭議的例子。但 Go 有很多魔法!你很容易就會遇到這樣的問題:無法做那些內建功能可以做的事情。

一些讓我印象深刻的地方:

  • 返回多個值和通道的語法很棒,但是這兩個無法一起使用,因為沒有元組型別。

  • 能夠用 for ... range 語句對陣列和切片進行迭代,但對其他集合就無能為力了,因為它缺乏迭代器的概念。

  • len 或者 append 這樣的函式是全域性函式,但你自己的函式卻無法轉變成全域性函式。這些全域性函式只能使用內建型別。即便 Go「沒有泛型」,它們也可以變得通用。

  • 沒有運算子過載,那麼 == 就會使人感到惱火。因為這意味著你不能在詞典中使用自定義型別作為鍵,除非它們是可比較的。這一屬性派生自型別結構,程式設計師無法重寫該屬性。

總結

Go 是一種簡單、小巧、令人愉悅的語言。它也有一些犄角旮旯,但絕大部分是經過精心設計的。它的學習速度令人難以置信,並且規避了其他語言中一些不那麼廣為人知的特性。

Go 也是一種與 Rust 截然不同的語言。雖然兩者都可以籠統地描述為「系統語言」或「C 語言的替代品」,但它們的設計目標、應用領域、語言風格和優先順序不盡相同。垃圾收集確實帶來了一個巨大的差異:使用 GC 使得 Go 變得更簡單、更小,也更容易理解。而不使用 GC 使 Rust 奇快無比(特別是在您需要保證延遲,而不僅僅是高吞吐量的時候),並且得以支援 Go 中不可能實現的特性或程式設計模式(或者至少在不犧牲效能的前提下是無法實現的)。

Go 是一種編譯型語言,其執行時得到了良好的實現,其速度毋庸置疑。Rust 也是編譯型語言,但是執行時要小得多,它真的迅捷無比。在沒有其他限制的情況下,我認為選擇使用 Go 還是 Rust 其實意味著一種權衡:一方面,Go 的學習曲線更短、程式更簡單(這意味著更快的開發速度);另一方面,Rust 真的效能卓越,並且型別系統更富有表現力(這使程式更安全,也意味著更快的除錯和錯誤查詢)。

?點選檢視 英文原版 文章

更多原創文章乾貨分享,請關注公眾號
  • 一位 Rust 開發者的 Go 初體驗
  • 加微信實戰群請加微信(註明:實戰群):gocnio