這是我們關于 DevOps 開發流程之中使用 Golang 之利與弊的六部曲系列。在這篇文章裡,我們會讨論 Golang 的運作時/編譯/維護的速度(好的方面);以及缺少泛型(缺點)。
在閱讀這篇文章之前,請確定你已經閱讀了上一篇關于“接口實作以及公有/私有命名方式”,或者訂閱我們的部落格更新提醒來擷取此六部曲後續文章的音訊。(我們會隔周更新,但是鑒于我們正在忙着釋出我們的 beta 平台我們的進度确實有點延後。)
- Golang 之于 DevOps 開發的利與弊(六部曲之一):Goroutines, Channels, Panics, 和 Errors
- Golang 之于 DevOps 開發的利與弊(六部曲之二):接口實作的自動化和公有/私有實作
- Golang 之于 DevOps 開發的利與弊(六部曲之三):速度 vs. 缺少泛型
- Golang 之于 DevOps 開發的利與弊(六部曲之四):time 包和方法重載
- Golang 之于 DevOps 開發的利與弊(六部曲之五):跨平台編譯,Windows,Signals,Docs 以及編譯器
- Golang 之于 DevOps 開發的利與弊(六部曲之六):Defer 指令和包依賴性的版本控
Golang 之利: 速度
我基于以往的經驗:在我們在寫 Blue Matador agent 之時在乎什麼,把速度這個優勢分解成三個不同的類别。(1)運作時,(2)編譯過程,(3)維護流程。确切資料的量會随着分類的遞進而減少 - 你會明白原因的.
運作時速度
因為我們的 agent 是跑在客戶伺服器之上,運作時速度是處于第一優先級的,除此之外還有安全和自動更新。很不幸的是,我們直到被我們的 Python 版本的 agent 狠狠坑了一次之後,才明白什麼方面才是第一優先級的。現在回顧,我很高興我們遠離了 Python 。
作為一個抽象化的運作時速度對比,我們從 http://benchmarksgame.alioth.debian.org 扒了一些資料然後放到了這個漂亮的圖表裡。這表顯示的是每種語言完成标準測試所花費的時間。花的時間越長,柱形會越高。注意 Python 在除了圓周率小數點計算(因為它給不出準确答案)之外的所有測試都遠遠落後。剩餘的語言全都處于一個檔次。
鑒于這是一篇讨論 Golang 速度的優點的博文,而不是探讨我們為什麼從 Python 遷移到 Golang ,我會去除兩種持續慢于 Golang 的語言: Python3 和 Node.js。在把幹擾項去掉之後,你會看見 Golang 和其他頂級競争者的結果:
圖中顯而易見的是,C++ 是最快的語言 - 完全不出意料。實際上,即使 Java 都把 Golang 擊敗了,但這出于兩個很好的理由:(1)Java 虛拟機(JVM)從1995年就開始開發了,比 Golang 多了 17 年。以及(2) 比起Golang ,JVM 在測試中花了 2 到 30 倍的記憶體使用量 - 這意味着總體上 Golang 的垃圾回收員比JVM的工作得更勤勞。
編譯速度
我們之前的 agent 是用 Python 寫的(并不是一個編譯語言),但這并不意味着我們不熟悉 Go 的編譯時優勢。
從 Golang 的一開始,較短的編譯時間一直是一個嚴苛的要求。 Go 是 Google 的 Ken Thompson 和 Rob Pike 創造的。Google,有超過20億行代碼,毫無疑問對于編譯所會浪費的時間極其嚴肅。
在這個文章裡,有一些非常棒的資訊,我也會在這總結。(我強力推薦你閱讀這個文章,因為它既精煉又細緻。)
首先看這幅關于一個相對大的代碼庫的編譯時間的圖。代碼細節可以在之前提到的連結中找到。注意看,Golang 在一衆競争者中的表現多棒,除了比不過 Pascal 這個被設計成隻為了跑一遍代碼的語言 - 基本上編譯時間會確定是 O(n)。考慮到 Golang 的語言規範依舊可用(不像 Pascal ),我會把 Go 微小的劣勢視為 Go 的勝利。
我不了解為什麼這次測試所有語言的編譯速度都如此快,但我明白至少一部分的原因是因為依賴管理系統。當一個檔案被編譯時,編譯器隻看這個檔案列出的直接依賴。它并不需要去遞歸加載所有依賴檔案的依賴。
Blue Matador Agent 有 29 個包,116824 行代碼。它還有 3 個目标作業系統,2 個目标架構,以及 3 個子產品。所有的這些都能在 10 秒内完成并行編譯(在一個 8 核的開發筆記本上)。好消息是我們幾乎不用花時間在編譯上。但壞消息是我們就沒時間像這幅 XKCD 漫畫一樣擊劍了。
維護速度
現在,轉到渾水區。在我開始細說之前,讓我先澄清幾點:
- 我們的 Golang 代碼庫是刻意保持在一個較小的體量
- 可能因為體量小,我們隻有 2 個被報告的 bug
- 目前我們有 3 個全職開發者,其中隻有兩個碰過 agent 的代碼
是以,當我說維護 Golang 代碼是非常簡單并且不費時間的時候,請了解我們是多麼缺少依據和經驗。關于這一點我可能完完全全是錯的。如果以後程式員們在詛咒着我的名字,我确信我會找到這個問題的答案。
盡管如此,以下是我認為 Go 的代碼維護效率更好的原因:
- 沒有記憶體管理。在 C/C++ 有很多代碼是隻為了記憶體管理而存在。你為了管理記憶體不得不做了一堆古怪的事。這在 Go 裡面,得益于垃圾回收機制,完全不是問題。
- 穩定可靠的核心庫。除了錯誤傳回系統之外,我們的代碼十分精簡。這是因為Go的核心庫有我們需要的所有東西; 從 HTTP 請求和 JSON 編碼/解碼到程序 fork 和 IPC 管道。
- 沒有泛型。對,我即将要說缺少泛型是這個語言的一個嚴重缺點。但是如果沒有泛型的話,變量類型全都會是明确且已知的。當你讀一個類檔案的時候,你會明确知道預期結果。這讓更改代碼更容易并且更快。
關于速度的額外閱讀資料
備注:我在我寫完這個文章之後才發現這個。我推薦你閱讀這篇文章,如果你對于 Go 的速度想知道更多:5 件使得Go很快的事
Golang 之弊:缺乏泛型
Golang 沒有泛型。我恨這點。毫無疑問這也是優點,但我真的很恨這點.
想象一下,在 C++ 下面工作卻沒有标準模闆庫(STL),并且還不允許你自己寫一份的情景。想象一下,你決定抛棄 Array 和 Hash map,轉而創造自己的資料結構,但卻發現你并不能在其他任何地方複用的心痛。想象一下,你擁有一個類型語言的所有優點,卻不能在一個強類型的類裡複用。這讓我想起了以前當我建立一些自定義網站的時候,我有寫代碼的一切能力,但我所能做的就隻是複制粘貼
functions.php
到我客戶的伺服器上。
當到了即将要寫一堆醜陋的代碼去繞過不能用泛型的限制之際,我做了(或者考慮過)去做以下這些事情使得 Go 能像我想象中一樣去運作。
Empty Interface
這個解決方案需要使用
empty interface
。類型為
interface{}
的變量可以是任何東西。這對于在規定變量類型有很好的靈活度。但同時,有各種各樣的原因會讓這個變得很糟糕。
如果你使用了一個
empty interface
作為數值的資料結構的話,那每次你需要和這個資料結構打交道的時候,你都要寫一堆廢話去做類型申明,而且這還不會傳回一個錯誤值。忽略潛在的錯誤會是一條直通災難的捷徑,因為一個簡單的重構就破壞了類型安全。
如果你在函數的傳入參數中使用了一個
empty interface
,你很有可能會在函數中有一個用于類型斷言的 switch 語句。在一個不同的變量類型下複用這樣的函數就意味着在
switch
語句中多加一行。我甯願去做一個腦白質切斷術,因為這真的不是“複用”,并且如果有越多開發者,這越行不通。
複制/粘貼
我們的免費 Watchdog 子產品檢測各類系統名額 - CPU,負荷,磁盤,網絡等等。我寫的第一個名額是 CPU 的各類占用率。當我寫這些的時候,我賦予它們
float32
類型。然後,下一個名額是目前運作的程序數,這明顯是一個屬于
unit
幹的活。
直到此時我才意識到 Go 跟我有私人恩怨.
我嘗試了許多不同的方法去讓查詢語句和持久層能同時和
float32
以及
unit
相容。我最不想做的就是複制/粘貼。幸運的是,我的故事是個成功的例子,我沒有複制/粘貼。我用了次優類型(馬上會談到)。但那曾經是多麼令人沮喪,我複制/粘貼了所有的代碼 - 所有的邏輯,解析,持久性等等 - 并且準備送出代碼。我真的做不到。
僅僅是 Go 語言規範幾乎讓我去送出一份複制/粘貼的複雜的查詢語句和持久層代碼這件事,就已經是一個讓我換另一種語言的強有力理由了.
次優類型
當時我沒有去複制/粘貼,而實際上我做的是在所有地方使用浮點數。沒錯, Watchdog 子產品使用浮點數去記錄目前運作程序數,磁盤寫入次數以及換入/換出次數。
這個辦法在數值類的類型中大都是沒有傷害的,這也就是為什麼我最終把它用在查詢語句與持久層之中。不适用這個辦法的情況其實遠多于那些适用的情況。如果你有數值類的類型,考慮下這個辦法。如果你沒有,你要回到
empty interface
加上類型斷言或者使用複制/粘貼.
至少,這比 CPU 占用率用 0-100 的 4 位元組整數表示來的要好。
Unions
隻是開個玩笑。Go 不支援 Unions,但這其實會非常有用。你認為呢?