GetStream.io:我們用 Go 替換 Python 的原因

tsteho發表於2019-04-19

切換到新的程式語言始終是一件大事,尤其是在這種嚴峻的情況下:團隊成員中僅有一人具備該種語言的使用經驗。今年年初,我們將 Stream’s 的主要程式語言從 Python 切換到了 Go。 這篇文章將給出一些理由以說明兩個問題:為什麼我們決定捨棄 Python?又是為什麼選擇了 Go?

Reasons to Use Go

為什麼使用 Go?

Reason 1 — Performance

原因 1 – 效能

Go 的執行速度非常快。效能類似於 Java 或者 C++。對於我們的使用情況來說,Go 一般比 Python 快 30 倍。這裡有個小型的測試遊戲 Go vs Java

原因 2 – 語言的效能很重要

對於很多應用來說,程式語言只是應用程式和資料庫之間的粘合劑。語言本身的效能通常無關緊要。

然而,Stream 是一家 API 提供商,其為 500 家公司和 2 億多終端使用者提供資訊流基礎設施。我們一直在優化 Cassandra、PostgreSQL 和 Redis 等工具。這持續了好幾年,但是最終,我們還是到達了所用語言的瓶頸。

Python 是一門很棒的語言,但是在諸如序列化/反序列化、排序以及聚合之類的場景上,它的效能相當差。我們常常遇到效能上的問題:花 1ms 的時間拿到 Cassandra 中的資料,Python 接下來還需要花 10ms 的時間將拿到的資料轉換成物件。

原因 3 – 開發人員的開發效率 & 拒絕太富有創造性

看看這些來自於 How I Start Go tutorial 的 Go 的程式碼片段。(這是一個很棒的教程,也是一個學習 Go 語言的一個很好的起點。)

如果你是 Go 語言新手,在閱讀那些程式碼片段時,沒有什麼會讓你大吃一驚。這些程式碼段僅僅演示了go語言的一些特性,例如賦值、資料結構、指標、格式化和內建的 HTTP 庫。

當我第一次開始程式設計時,我總是喜歡用 Python 比較高階的特性。Python 允許你更有“創意”的寫程式碼。比如,你能夠做如下的事:

  • 在程式碼初始化時使用元類自行註冊類
  • 關鍵字 True 和 False 的值可以互換
  • 編寫自己的函式,並且使其成為內建函式。
  • 通過魔法方法過載運算子

這些特性很有趣,但是,正如大多數程式設計師都同意的那樣,在閱讀別人的程式碼時,它們的存在使得程式碼更難理解。

Go 迫使你迴歸基礎。這決定了 Go 程式碼是容易閱讀和理解的。

說明:當然,“容易”的程度需要視情況而定。如果你想要建立一個基本的增刪改查介面,我仍然推薦你使用 Django + DRF,或者用 Rails。

原因 4 – 併發 & 管道

作為一門程式語言,Go 試圖讓事情變得簡單。它沒有引入很多的新概念。重點是創造的這門程式語言的效能要難以置信的快,並且容易上手。goroutines 和管道是 Go 僅有的創新點。(準確的講,CSP 這個概念1977年就被提出了,因此這個創新更準確的說法是——舊點子的新實現)Goroutines 是 Go 對執行緒的輕量級實現,而管道是讓 goroutines 之間相互通訊的絕佳的方式。

Goroutines 佔用的資源非常少,只需要幾 KBs 的額外記憶體。因為 Goroutines 非常輕量,所以同時執行數百甚至數千個也不在話下。

你可以使用管道在 goroutines 之間通訊。Go 執行時會處理所有的複雜事物。goroutines 的存在以及基於管道的併發方法,使得程式可以充分利用 CPU 資源、處理併發 IO — 所有這些都沒有增加開發的複雜性。與 Python/Java 相比,在 goroutine 上執行一個函式只需要非常少的樣板程式碼。您只需在函式呼叫前加上關鍵字“go”:

https://tour.golang.org/concurrency/1

和 Node 相比,Go 的併發處理更加容易。如果使用 Node 寫併發方法,開發者必須密切關注非同步程式碼的處理方式。

Go 自帶競爭檢測器,這是用 Go 寫併發程式另一個好的方面。如果非同步程式碼中出現條件競爭的情況,檢測器能幫你輕鬆地找到問題。

如果要學習 Go 和管道的話,下面是一些不錯的資料:

原因 5 – 編譯時間短

目前,我們使用 Go 編寫的最大微服務只需 6 秒鐘就能完成編譯。與 Java 和 C++ 這樣以低速編譯速度著稱的語言相比,Go 的快速編譯能力是一場生產力上的大勝。我也喜歡趁著程式碼編譯的時間去放鬆一下,但是,如果能在我還記得程式碼是做什麼事情的時候就完成編譯,豈不更好?況且本來就應該是這樣的才對。

原因 6 – 建立一個團隊的能力

首先,讓我們認清一個現實:與 C++ 和 Java 這樣的老牌程式語言相比,Go 開發人員的數量是不佔上風的。根據 StackOverflow 的資料,38% 的開發人員熟悉 Java,19.3% 的開發人員熟悉 C++,僅僅 4.6% 的開發人員熟悉 Go。GitHub 上的資料顯示一個相似的趨勢: Go 用得比 Erlang、Scala 以及 Elixir 廣泛,但是不及 Java 和 C++。

幸運的是,Go 很簡單,而且易於學習。它提供了你所需要的基本的特性,一點不多,一點不少。它引入了 2 個新的概念:“defer”宣告、“go routines” 和管道內建的併發管理。(對於純粹主義者來說:Go 並不是第一種實現這些概念的語言,而是第一種使它們受歡迎的語言。)團隊中任何地 Python、Elixir、C++、Scala 或 Java 開發人員都可以在一個月內有效地掌握 Go,因為它非常簡單。

我們發現,和很多其他的程式語言相比,建立一個 Go 開發團隊更容易。如果你在競爭激烈的環境(如 Boulder、Amsterdam)僱傭人員,這是一大優點。

原因 7 – 強大的生態系統

對於我們一個大約 20 個人的團隊來說,生態系統很重要。如果你不得不重新發明每一部分的功能,你根本不可能為你的客戶創造價值。Go 對我們使用的工具提供了很大的支援。比如這些可靠的庫:Redis、RabbitMQ、PostgreSQL、模板解析、任務排程、表示式解析和 RocksDB。

與 Rust 或 Elixir 等其他新語言相比,Go 的生態系統是一項重大勝利。當然,Go 並不像 Java、Python 或者 Node 那樣出色。但是它非常的可靠,並且對於一些基本的需求,你都可以找到高質量的包。

原因 8 – Gofmt:強制程式碼格式化

那麼什麼是 Gofmt 呢?注意,它並不是髒話。Gofmt 是一個極棒的命令列工具集,已整合到了 Go 編譯器,用於格式化程式碼。從功能上來講,它有點像 Python 中的 autopep8。除非是在《矽谷》電視劇中,不然大多數人並不真的喜歡爭論該用 tabs 還是 spaces。格式的一致性是非常重要的,但是實際的格式標準並不是那麼重要。Gofmt 提供官方的標準來格式化你的程式碼,從而避免了不必要的爭論。

原因 9 – gRPC 與 Protocol Buffers

Go 對 protocol buffers 和 gRPC 有著一流的支援。在構建需要通過 RPC 進行通訊的微服務時,這兩個工具可以很好地協同工作。你只需編寫一個說明檔案,裡面只需定義可以進行的 RPC 呼叫以及它們採用的引數。根據這份說明檔案,伺服器和客戶端程式碼就會自動生成。由此產生的程式碼執行快速,網路佔用空間小,易於使用。

根據相同的說明檔案,甚至可以生成很多不同程式語言的客戶端程式碼,比如 C++、Java、Python 和 Ruby。因此,內部流量不再有模糊的 REST 終端,因為你不必每次都寫一遍幾乎相同的客戶端和伺服器端程式碼。

使用 Golang 的缺點

缺點 1 – 缺少框架

Go 沒有一個具有代表性的框架,像 Ruby 有 Rails、Python 有 Django 或者 PHP 有 Laravel。在 Go 社群中,這是一個爭論激烈的話題,很多人提倡不應該一開始就使用框架。某些使用案例,我完全同意這樣的觀點。然而,如果只是想要建立一個增刪改查的介面,使用 Django/DJRF、Rails Laravel 或者 Phoenix 是一個更好的選擇。

缺點 2 – 錯誤處理機制

Go 處理錯誤的過程如下:簡單地從函式中返回錯誤,並且期望你呼叫程式碼來處理該錯誤(或者將它返回到呼叫堆疊之上)。雖然這種方法有效,但很容易丟失出錯的範圍,導致無法為使用者提供有意義的錯誤。 errors 包通過允許你為錯誤新增上下文和堆疊來跟蹤問題。

另一個問題是很容易忘記處理錯誤。像 errcheck 和 megacheck 這樣的靜態分析工具可以方便地規避這些錯誤。

雖然這些解決方法很有效,但總感覺哪裡不太對勁。 你肯定希望語言本身就支援一定的錯誤處理的功能。

缺點 3 – 包管理

Go 的包管理肯定不是完美的。預設情況下,它沒有辦法指定依賴項的特定版本,也沒有辦法建立可重現的構建。 Python、Node 和 Ruby 都有更好的包管理系統。然而,通過合適的工具,Go 的包管理表現的很好。

你可以使用 Dep 來管理依賴項以允許指定和固定版本。 除此之外,我們還提供了一個叫做 VirtualGo 的開源工具,它可以更輕鬆地處理用 Go 編寫的多個專案。

Python vs Go

我們之前做過一個有趣的實驗:選擇我們的 ranked feed 功能,用 Go 語言將它重寫。簡單看下這個排名方法的例子:

為了使這個排名方法成立,Python 和 Go 都需要遵循下面的事:

  1. 解析表示式以便打分。在這種情況下,我們希望將“simple_gauss(time)* popular”這個字串轉換成一個函式:函式以一個活動作為輸入,然後返回一個分數作為輸出。
  2. 基於 JSON 配置建立偏函式。比如:我們想要“simple_gauss”呼叫“decay_gauss”,並傳遞規模為 5 天,偏差為 1 天,衰減係數為 0.3 這些引數。
  1. 解析“預設值”配置,以便在活動中出現未定義欄位時可以進行回退。
  2. 使用步驟 1 中的函式給流中的所有活動打分。

開發 Python 版本的排名程式碼大約需要 3 天。這包括編寫程式碼、單元測試和文件書寫。接下來,我們花了大約 2 周時間來優化程式碼。其中一個優化是將評分表示式(simple_gauss(time)*popularity)轉換為抽象語法樹。我們還實現了快取邏輯,該邏輯在將來的某些時間預先計算得分。

相比之下,開發該程式碼的 Go 版本大約需要 4 天時間。效能不需要任何進一步的優化。因此,雖然 Python 初始的開發速度更快些,但如果基於 Go 的版本,最終,我們團隊的工作量大大減少。作為額外的優點,Go 程式碼的執行速度比我們高度優化的 Python 程式碼快大約 40 倍。

這只是一個簡單的說明效能提升的例子:僅僅用 Go 替換 Python。 當然,它們沒有可比性:

  • 排名程式碼是我第一個用 Go 寫的專案
  • Go 程式碼是在 Python 程式碼之後構建的,因此我可以更好地理解用例
  • 用於表示式解析的 Go 庫是非常高質量的

具體細節需要視情況而定。和 Python 相比,用 Go 構建一些我們系統中其他的元件,需要花費更多的時間。一般情況下,我們發現用 Go 開發程式碼更費些勁。然而,在效能方面,我們花費更少的時間來優化程式碼。

Elixir vs Go

我們評估了另一種語言:Elixir。Elixir 構建於 Erlang 虛擬機器之上。這是一種引人入勝的語言。我們考慮過它,因為我們團隊成員中有一個人擁有大量的 Erlang 經驗。

對於我們的用例,我們注意到 Go 的原始效能要好得多。Go 和 Elixir 都可以很好地為數千個併發請求提供服務。但是,如果你檢視單個請求的效能,Go 對我們的用例來說要快得多。生態系統是另一個我們選擇 Go 而不選擇 Elixir 的原因。對於我們需要的元件,Go 有更多成熟的庫,而在許多情況下,Elixir 庫還沒有為生產使用做好準備。培訓/招聘用 Elixir 的開發人員也更難。

這些原因讓我們選擇了 Go。雖然 Elixir 的 Phoenix 框架看起來非常棒,並且也絕對值得一看。

總結

Go 是一種非常高效的語言,且對併發性有很大的支援。它的效能幾乎與 C++ 和 Java 等語言一樣快。雖然和 Python 或 Ruby 相比,使用 Go 構建內容需要花費更多時間,但你將節省大量時間來優化程式碼。

我們在 Stream 有一個小型的開發團隊,為超過2億的終端使用者提供資訊流。擁有一個偉大的生態系統、新開發人員容易上手、快速的效能、對併發性的可靠支援以及高效的程式設計環境,使 Go 成為一個很好的選擇。

Stream 仍然利用 Python 為我們的控制皮膚、站點和機器學習提供個性化的流。 我們不會很快告別 Python,但是所有效能密集型程式碼都將用 Go 編寫。

如果你想要了解更多有關 Go,檢視下面列出的部落格文章。如果你想要了解 Stream,這個互動教程是一個好的起點。

More Reading about Switching to Golang

關於切換到 Golang 的更多閱讀

Learning Go

學習 Go

這篇文章最初由 Thierry Schellenbach 撰寫,GetStream.io 的 CEO。原始博文的地址是https://getstream.io/blog/switched-python-go/

相關文章