Categories
程式開發

一位 Rust 開發者的 Go 初體驗


過去幾週,我一直在用 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 真的性能卓越,並且類型系統更富有表現力(這使程序更安全,也意味著更快的調試和錯誤查找)。

作者介紹

Nick Cameron,PingCAP 研發工程師,Rust 語言核心成員。

本文轉載自公眾號PingCAP(ID:pingcap2015)。

原文鏈接

https://mp.weixin.qq.com/s/UDw0Me_heGaJcUt1MLjDuA