為什麼我們放棄 Python 而選擇 Go?(getstream.io 的架構變遷)

originator發表於2020-02-16

原文地址:https://getstream.io/blog/switched-python-go/

更新於2019年5月14日, 為了更好的反映過去兩年 Go 的提升(包管理,更好的效能,更快的編譯時間和更成熟的生態系統)。 切換到新的程式語言總歸來說是一大步改動,特別是團隊就你一人有該語言的使用經驗。年初,我們把 Stream’s 的主要程式語言從 Python 切換到 Go。這篇文章將解釋為甚我們決定放棄 Python 並轉而使用 Go。

理由一 - 效能

Go 很快!Go 是相當的快。其效能比肩 Java 或 C++。在我們的用例中,Go 通常比 Python 快 40 倍。這裡有一個 Go vs Python 小的基準比較遊戲。

理由二 - 語言效能很重要

對於大部分應用來說,程式語言只是應用程式和資料庫之間的粘合劑。語言本身的表現通常無關緊要。然而,Stream 一個提供 API 的程式,為 700 家公司和 5 億多終端使用者提供 動態流 和 實時聊天 的基礎設施。多年來,我們一直在優化 Cassandra、PostgreSQL、Redis 等,但最終,達到了所使用語言的極限。Python 是一種很棒的語言,但是對於序列化 / 反序列化、排名和聚合等用例來說,它的效能相當緩慢。我們經常遇到效能問題,Cassandra 需要 1 毫秒來檢索資料,而 Python 則需要 10 毫秒來將其轉換為物件。

理由三 - 開發人員的生產率和創新能力不足

package main
type openWeatherMap struct{}
func (w openWeatherMap) temperature(city string) (float64, error) {
    resp, err := http.Get("http://api.openweathermap.org/data/2.5/weather?APPID=YOUR_API_KEY&q=" + city)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    var d struct {
        Main struct {
            Kelvin float64 json:"temp"
        } json:"main"
    }
    if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
        return 0, err
    }
    log.Printf("openWeatherMap: %s: %.2f", city, d.Main.Kelvin)
    return d.Main.Kelvin, nil
}

即便是 Go 語言的新手,在閱讀這小程式碼片段時,也不會出現難以理解的地方。它演示了多賦值函式,資料結構,指標,格式和內建的 HTTP 庫。剛開始程式設計時,我總是喜歡使用 Python 的高階功能。使用 Python 編碼時你可以發揮創意。例如,你可以:

  • 在程式碼初始化時使用 MetaClasses 自注冊類
  • 交換 True 和 False
  • 將函式新增到內建函式列表中
  • 通過魔術方法過載運算子
  • 通過 @property 裝飾器將函式用作屬性

這些功能很有趣,但是正如大多數程式設計師所同意的那樣,它們通常使程式碼可讀性降低,使你難以理解別人的工作。 Go 迫使您堅持基礎知識。這樣一來,您就可以輕鬆閱讀任何人的程式碼並立即瞭解發生了什麼。** 注:到底有多 “簡單” 取決於您的用例。如果您想建立基本的 CRUD API,我仍然建議您使用 Django + DRF 或 Rails。**

理由 4 - 併發與通道

作為一種語言,Go 儘量使事情簡單化。它沒有引入很多新概念。Go 語言的作者致力於建立一種,速度非常快,並且易於使用的語言。它唯一創新的領域是協程和通道。(準確的來說,CSP 的概念始於 1977 年,所以這一創新更多的是一種對舊思想的新方法。) 協程是 Go 的執行緒化輕量級方法,通道是協程之間通訊的首選方式。協程的建立成本很低,只需要幾 KB 的額外記憶體。因為協程是輕量級的,所以有可能同時執行數百甚至數千個協程。您可以使用通道在協程之間進行通訊。Go 執行時處理所有的複雜性。協程和基於通道的併發方法使得使用所有可用的 CPU 核心和處理併發 IO 變得非常容易,而不需要複雜的開發。與 Python/Java 相比,在協程上執行函式只需要少量的程式碼。你只需在函式呼叫前加上關鍵字「go」:

package main
import (
    "fmt"
    "time"
)
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}
func main() {
    go say("world")
    say("hello")
}

https://tour.golang.org/concurrency/1 Go 的併發方法非常容易使用。與開發人員必須密切關注非同步程式碼如何處理的 Node 相比,這是一種有趣的方法。Go 中併發性的另一個重要方面是競態檢測器。這樣就很容易判斷非同步程式碼中是否存在競爭條件。

咚咚!(敲門聲) 競爭條件你在哪? — 我是開發人員 (@iamdevloper) November 11, 2013

這裡有一些好的資源可以幫助你開始使用 Go 和通道:

理由 5 - 快速的編譯時間

我們用 Go 編寫的最大的微服務目前需要 4 秒來編譯。與 Java 和 C++ 等以編譯速度緩慢著稱的語言相比,Go 的快速編譯時間是提高生產力的一大優勢。我喜歡劍鬥,但是在我還記得程式碼應該做什麼的情況下完成工作會更好:

理由 6 - 建立一個團隊的能力

首先,讓我們從一個明顯的事實開始:與 C++ 和 Java 等老牌的語言相比,Go 開發人員並不多。根據 StackOverflow 來看, 38% 的開發者知道 Java, 19.3% 的知道 C++ ,只有僅僅 4.6% 的知道 Go。GitHub 資料顯示了一個類似的趨勢:Go 比 Erlang、Scala 和 Elixir 等語言使用得更廣泛,但不如 Java 和 C++ 流行。幸運的事,Go 是一門非常簡單易學的程式語言。它只提供了您需要的基本功能。它引入的新概念是「defer」語句和內建的「go 協程」和通道併發管理。(對於純粹主義者來說:Go 並不是實現這些概念的第一種語言,它只是使這些概念流行起來的第一種語言。) 任何加入團隊的 Python、Elixir、C++、Scala 或 Java dev 都可以在一個月內高效地使用 Go,因為它非常簡單。我們發現,與其他語言相比,建立一個 Go 開發團隊更容易。如果你在競爭激烈的招聘環境博爾德和阿姆斯特丹中招聘員工,這是非常有益的。

理由 7 - 強大的生態系統

對於我們這樣規模的團隊 (約 20 人) 來說,生態系統很重要。如果你必須重新設計每一個小功能,你就無法為你的客戶創造價值。Go 為我們使用的工具提供了強大的支援。Go 可靠的庫已經可以用於 Redis、RabbitMQ、PostgreSQL、模板解析、任務排程、表示式解析和 RocksDB。與 Rust 或 Elixir 等新語言相比,Go 的生態系統是一個重大的勝利。它當然沒有 Java、Python 或 Node 等語言那麼好,但它是可靠的,對於許多基本需求,您可以找到高質量的包。

理由 8 - Gofmt, 強制格式化程式碼

讓我們從什麼是 Gofmt 開始?不,這不是罵人的話。Gofmt 是一個非常棒的命令列實用工具,內建在 Go 編譯器中,用於格式化程式碼。在功能方面,它與 Python 的 autopep8 非常相似。儘管矽谷的描述與此不同,但我們大多數人並不是真的喜歡討論製表符與空格之間的區別。格式的一致性很重要,但是實際的格式標準並沒有那麼重要。Gofmt 通過一種官方的格式來避免這些討論。

理由 9 - gRPC 和協議緩衝區

Go 對協議緩衝區和 gRPC 有一流的支援。這兩個工具在構建需要通過 RPC 進行通訊的微服務時配合得非常好。您只需要編寫一個清單,其中定義可以進行的 RPC 呼叫,以及它們採用的引數。然後,伺服器和客戶端程式碼都會從這個清單中自動生成。這個生成的程式碼速度很快,佔用的網路空間很小,而且很容易使用。通過相同的清單,您甚至可以為許多不同的語言 (如 C++、Java、Python 和 Ruby) 生成客戶端程式碼。因此,對於內部通訊流,不再有不明確的 REST 端點,您也不必每次都編寫幾乎相同的客戶端和伺服器程式碼。

缺點 1 - 缺少框架

Go 缺少一個主流的框架,比如 Ruby 的 Rails、Python 的 Django 或 PHP 的 Laravel。這是 Go 社群中的一個熱議話題,因為許多人主張不應該從使用框架開始。我完全同意在某些用例中是這樣的。但是,如果有人想構建一個簡單的 CRUD API,那麼他們將更容易使用 Django/DJRF、Rails、Laravel 或 Phoenix .

更新: 正如評論指出的 目前有幾個不錯的 Go 框架。Beego, Revel,Iris,Echo、Macaron 和 Buffalo 似乎都是不錯的選項。

對於 Stream 的用例,我們更喜歡不使用框架。然而,對於許多希望提供簡單 CRUD API 的新專案來說,缺少一個主導框架將是一個嚴重的缺點。

缺點 2 - 錯誤處理

Go 通過簡單地從一個函式返回一個錯誤並期望你的呼叫程式碼來處理這個錯誤 (或者將它返回到呼叫堆疊) 來處理錯誤。雖然這種方法有效,但是不能有效的定位錯誤位置,從而無法確保您可以向使用者提供有意義的錯誤。錯誤包解決了這個問題,它允許您向錯誤新增上下文和堆疊跟蹤。另一個問題是它很容易忘記處理意外的錯誤。像 errcheck 和 megacheck 這樣的靜態分析工具可以很方便地避免犯這些錯誤。雖然這些變通方法很有效,但感覺卻不太對。您希望該語言支援正確的錯誤處理。

缺點 3 - 包管理

更新:自本文撰寫以來,Go 的包管理系統已經有了很大的進步。Go modules 是一個有效的解決方案,我看到的唯一問題是它們破壞了一些靜態分析工具,比如 errcheck。這裡有一個學習使用 Go 使用 Go modules 的教程。Go 的包管理絕不是完美的。預設情況下,它無法指定依賴項的特定版本,也無法建立可複製的構建。Python、Node 和 Ruby 都有更好的包管理方式。然而,只要有合適的工具,Go 的包管理就可以很好地工作。您可以使用 Dep 來管理依賴項,以允許指定和固定版本。除此之外,我們還提供了一個叫做 VirtualGo 的開源工具,它可以讓你更輕鬆地在多個專案中工作。

Python vs Go

更新:自本文撰寫以來,Python 和 Go 之間的效能差異有所增加。(Go 變得更快了,而 Python 沒有) 我們進行了一個有趣的實驗,用 Python 實現了動態流排行功能,然後用 Go 重寫它。看看這個排名方法的例子:

{
    "functions": {
        "simple_gauss": {
            "base": "decay_gauss",
            "scale": "5d",
            "offset": "1d",
            "decay": "0.3"
        },
        "popularity_gauss": {
            "base": "decay_gauss",
            "scale": "100",
            "offset": "5",
            "decay": "0.5"
        }
    },
    "defaults": {
        "popularity": 1
    },
    "score": "simple_gauss(time)*popularity"
}

Python 和 Go 程式碼都需要執行以下操作來支援這種排序方法:

  1. 解析表示式以獲取分數。在本例中,我們希望將這個字串「simple_gauss (time)* popularity」轉換為一個函式,該函式將活動作為輸入返回分數作為輸出。
  2. 基於 JSON 配置建立區域性函式。例如,我們希望「simple_gauss」以 5 天的比例,1 天的偏移量和 0.3 的衰減因子來呼叫「decay_gauss」。
  3. 解析「預設」配置,這樣,如果某個欄位沒有在某個活動上定義,您就可以進行回退。
  4. 使用步驟 1 中的函式對提要中的所有活動進行評分。

開發 Python 版本的排名程式碼大約需要 3 天。這包括編寫程式碼,單元測試和文件。接下來,我們花了大約 2 周的時間來優化程式碼。一種優化是將得分表示式 simple_gauss(time)* popularity) 轉換為抽象語法樹。我們還實現了快取邏輯,該邏輯可在將來的某些時間預先計算分數。相反,開發此程式碼的 Go 版本大約需要 4 天。效能不需要任何進一步的優化。因此,雖然 Python 的初始開發速度更快,但基於 Go 的版本最終需要我們團隊做的工作卻少得多。另外一個優勢是,Go 程式碼的執行速度比高度優化的 Python 程式碼快 40 倍。現在,這只是我們切換到 Go 所獲得的效能提升的一個示例。當然,這是將蘋果與橙子進行比較:

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

速度的優勢需視場景而定。與 Python 相比,我們系統的其他一些元件在 Go 中構建花費的時間要多得多。作為一個大趨勢,我們發現開發 Go 程式碼開發起來需要花費更多的時間。但是,我們花費更少的時間 優化 程式碼的效能。

Elixir vs Go - 誰跑的更快

我們評估的另一種語言是 Elixir. Elixir 構建在 Erlang 虛擬機器之上。這是一種令人著迷的語言,因為我們的團隊成員之一對 Erlang 有著豐富的經驗,因此我們考慮了這一點。對於我們的用例,我們注意到 Go 的原始效能要好得多。 Go 和 Elixir 都將在處理數千個併發請求方面做得很好。但是,如果您檢視單個請求的效能,那麼對於我們的用例而言,Go 實質上要快得多。我們選擇 Go over Elixir 的另一個原因是生態系統。對於我們所需的元件,Go 具有更成熟的庫,而在許多情況下,Elixir 庫尚未準備好用於生產。培訓 / 尋找開發人員與 Elixir 合作也更加困難。這些原因使 Go 變得更加平衡。不過,Elixir 的 Phoenix 框架看起來很棒,絕對值得一看。

結論

Go 是一種效能非常好的語言,具有很好的併發性支援。它幾乎與 C + 和 Java 等語言一樣快。雖然與 Python 或 Ruby 相比,使用 Go 構建東西確實需要更多的時間,但您可以節省大量的時間用於優化程式碼。我們在 Stream 有一個小型開發團隊,為超過 5 億的終端使用者提供 feed 和 chat 。 Go 結合了強大的生態系統,新開發者易於入門,快速效能,併發可靠支援和高效的程式設計環境的組合,使其成為絕佳的選擇。 Stream 仍然將 Python 用於個性化提要的儀表板,站點和機器學習。 我們不會在不久的將來與 Python 道別,但今後所有效能密集型程式碼都將用 Go 編寫。我們的新 Chat API 也完全用 Go 編寫。如果您想了解有關 Go 的更多資訊,請檢視下面列出的部落格文章。 如果您想了解有關 Go 的更多資訊,請檢視下面列出的部落格文章。要了解有關 Stream 的更多資訊,此互動式教程是一個很好的起點。

更多原創文章乾貨分享,請關注公眾號
  • 為什麼我們放棄 Python 而選擇 Go?(getstream.io 的架構變遷)
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章