【感謝張佳偉(@ghosert)的熱心翻譯。如果其他朋友也有不錯的原創或譯文,可以嘗試推薦給伯樂線上。】
這是一篇(長)博文, 介紹了我們在 Repustate 遷移大量 Python/Cython 程式碼到 Go 語言的經驗。如果你想了解整個故事,背景和所有的事情,請繼續往下讀。如果你只是想了解 Python 開發者在一頭扎進 Go 語言前需要了解什麼,請點選一下連結:
從Python遷移到Go的建議(Tips & Tricks)
背景
在 Repustate,我們完成過的最棒的技術成就之一是實現了阿拉伯語的情感分析。阿拉伯語是一塊難啃的硬骨頭,因為它的詞形變化相當複雜。比起譬如英語,阿拉伯語的分詞(將一個句子切分呈幾個獨立的單詞)也更困難,因為阿拉伯語的單詞本身還可能會包含空白字元(例如:“阿列夫”在一個單詞裡的位置)。這也談不上是洩密,Repustate 使用支援向量機(SVM)來獲取一個句子背後最有可能的含義,並在其中加上情感元素。 總體上來說,我們使用了 22 種模型(22 個 SVM) 並且在一篇文件中,每一個單詞我們都會加以分析。因此如果你有一篇 500 字的文件,那麼基於 SVM,會進行十萬次的比較。
Python
Repustate 幾乎完全就是一個 Python 商店。我們使用 Django 來實現 API 和網站。因此(目前)為了保持程式碼一致,同時使用 Python 來實現阿拉伯語情感引擎是合情合理的。只是做原型和實現的話,Python 是很好的選擇。它的表達能力很強悍,第三方類庫等等也很好。如果你就是為了Web服務,Python 很完美。但是當你進行低階別的計算,大量依賴於雜湊表(Python 裡的字典型別)做比較的時候,一切都變慢了。我們每秒能處理大約兩到三個阿拉伯文件,但是這太慢了。比較下來,我們的英語情感引擎每秒能處理大約五百份文件。
瓶頸
因此我們開啟了 Python 分析器,開始調查是什麼地方用了那麼長時間。還記得我前面說過我們有 22 個 SVM 並且每個單詞都需要經過處理嗎?好吧,這些都是線性處理的,非並行處理。所以我們的第一反應是把線性處理改成 map/reduce 那樣的操作。簡單來說:Python 不太適合用作 map/reduce。當你需要併發的時候,Python 算上好用。在 2013 Python 大會上(譯者:PyCon 2013),Guido 談到了 Tulip,他的這個新專案正在彌補 Python 這方面的不足,不過得過段一段時間才能推出,但是如果已經有了更好用的東西,我們為什麼還要等呢?
選 Go 語言,還是回家算了?
我在Mozilla的朋友告訴我,Mozilla 內部正在將他們大量的基礎日誌架構切換到 Go 語言上,部分原因是因為強大的 [goroutines]。Go 語言是 Google 的人設計的,並且在設計之初就把支援併發作為第一要務,而不是像 Python 的各種解決方案那樣是事後才加上去的。因此我們開始著手把 Python 換成 Go 語言。
雖然 Go 程式碼還不算正式上線的產品,但是結果非常令人鼓舞。我們現在能做到每秒處理一千份文件,使用更少的記憶體,還不用除錯你在 Python 裡遇到:醜陋的多程式/gevent/“為什麼 Control-C 殺不了程式”這些問題。
為什麼我們喜歡 Go 語言
任何人,對程式語言是如何工作(解釋型 vs 編譯型, 動態語言 vs 靜態語言)有一點理解的話,會說,“切,當然 Go 語言會更快”。是的,我們也可以用 Java 把所有的東西重寫一遍,也能看到類似更快的改善,但那不是 Go 語言勝出的原因。你用 Go 寫的程式碼好像就是對的。我搞不清楚到底是怎麼回事,但是一旦程式碼被編譯了(編譯速度很快),你就會覺得這程式碼能工作(不只是跑起來不會錯,而且甚至邏輯上也是對的)。我知道,這聽上去不太靠譜,但是確實如此。這和 Python 在冗餘(或非冗餘)方面非常類似,它把函式作為第一目標,因此函式程式設計會很容易想明白。而且當然,go 執行緒和通道讓你的生活更容易,你可以得到靜態型別帶來的效能大提升,還能更精細的控制記憶體分配,而你卻不必為此在語言表達力上付出太多的代價。
希望能早點知道的事情(Tips & Tricks)
除去所有這些讚美之詞以後,有時你真的需要在處理 Go 程式碼的時候,相對於 Python,改變一下思維方式。因此這是我在遷移程式碼時記錄的筆記清單 —— 只是在我把 Python 程式碼轉換到 Go 時從我腦子裡隨機冒出來的點子:
- 沒有內建的集合型別(必須使用map,並檢查是否存在)
- 因為沒有集合,必須自己寫交集,並集之類的方法
- 沒有 tuples 型別,必須寫你自己的結構,或者使用 slices (即陣列)
- 沒有類似 \__getattr__() 的方法,你必須總是檢查存在性,而不是設定預設值,例如,在 Python 裡,你可以這樣寫 value = dict.get(“a_key”, “default_value”)
- 必須總是檢查錯誤(或者顯式的忽略錯誤)
- 不能有變數/包沒被使用,因此簡單的測試也需要有時注掉一些程式碼
- 在 [] byte 和 string 之間轉換。 regexp 使用 [] byte (不可變)。這是對的,但是老把一些變數轉換來轉換去很煩人
- Python 更寬鬆。你可以使用超出範圍的索引在字串裡取一個片段,而且不會出錯。你還可以用負數取出片段,但是 Go 不行
- 你不能混合資料結構型別。也許這樣也不太乾淨,但是有時在 Python 裡,我會使用值是混合了字串和列表的字典。但是 Go 不行,你不得不清理乾淨你的資料結構或者使用自定義的結構
- 不能解包一個 tuple 或者 list 到幾個不同的變數(例如:x, y, z = [1, 2, 3])
- 駝峰式命名風格(如果你沒有首字大寫方法名/結構名,他們不會被暴露給其它的包)。我更喜歡 Python 的小寫字母加下劃線命名風格。
- 必須顯式檢查是否有錯誤 != nil, 不像在 Python 裡,許多型別可以像 bool 那樣檢查 (0, “”, None 都可以被解釋成 “非” 集合)
- 文件在一些模組上太散亂了,例如(crypto/md5),但是 IRC 上的 go-nuts 很好用,提供了巨大的幫助。
- 從數字到字串的轉換(int64 -> string) 和 []byte -> string (只要使用 string([]byte))不太一樣。需要使用 strconv。
- 閱讀 Go 程式碼比起 Python 那樣寫起來如虛擬碼的語言更像一門程式語言, Go 有更多的非字母數字字元,並且使用 || 和 &&, 而不是 “or”和“and”
- 寫一個檔案的話,有 File.Write([]byte) 和 File.WriteString(string), 這點和 Python 開發者的 Python 之道:“解決問題就一種方法 ”相違背。
- 修改字串很困難,必須經常重排 fmt.Sprintf
- 沒有建構函式,因此慣用法是建立 NewType() 方法來返回你要的結構
- Else (或者 else if)必須正確格式化,else 得和 if 配對的大括號在同一行。奇怪。
- 賦值運算子取決於在函式內還是函式外,例如,= 和 :=
- 如果我只想要“鍵”或者只想要 “值”,譬如: dict.keys() 或者 dict.values(),或者一個 tuples 的列表,例如:dict.items(),在 Go 語言裡沒有等價的東西,你只能自己列舉 map 來構造你的列表型別
- 我有時使用一種習慣用法:構造一個值是函式的字典型別,我想通過給定的鍵值呼叫這些函式,你在 Go 裡可以做到,但是所有的函式必須接受,返回相同的東西,例如:相同的方法簽名
- 如果你使用 JSON 並且 你的 JSON 是一個複合型別,恭喜你。 你必須構造自定義的結構匹配 JSON 塊裡的格式,然後把原始 JSON 解析到你自定義結構的例項中去。比起 Python 世界裡 object = json.loads(json_blob) 要做更多的工作
是不是值得?
值得,一百萬倍的值得。速度的提升太多了,以致很難捨棄。同時,我認為, Go 是目前趨勢所在,因此在招新員工的時候,我認為把 Go 當作 Repustate 技術積累的重要一環會很有幫助。