客觀對比Node 與 Golang

chengkai發表於2018-07-11

Go僅用標準庫就能寫大多數的軟體。用Node.js時,我們幾乎都是不得不引入一個外部的庫, 這樣做既增加了部署的時間,也增加了來自第三方軟體的潛在隱患。只用標準庫能讓我們寫的程式碼更快更安全。

另外值得一提的是,node的程式碼在跨平臺執行時,有時會碰到各種意想不到的問題,而go語言的程式碼實現跨平臺執行時僅僅配置一下gopath即可完美相容。

d

包管理

Go對包管理一定有自己的理解。對於包的獲取,就是用go get命令從遠端程式碼庫(GitHub, Bitbucket, Google Code, Launchpad)拉取。並且它支援根據import package分析來遞迴拉取。這樣做的好處是,直接跳過了包管理中央庫的的約束,讓程式碼的拉取直接基於版本控制庫,大家的協作管理都是基於這個版本依賴庫來互動。細體會下,發現這種設計的好處是去掉冗餘,直接複用最基本的程式碼基礎設施。Go這麼幹很大程度上減輕了開發者對包管理的複雜概念的理解負擔,設計的很巧妙。

但是這樣也會產生一系列的問題:

  • 缺乏明確顯示的版本。團隊開發不同的專案容易匯入不一樣的版本,每次都是get最新的程式碼。尤其像我司對開源軟體管理非常嚴格,開源申請幾乎是無法實施。

  • 第三方包沒有內容安全審計,獲取最新的程式碼很容易引入程式碼新的Bug,後續執行時出了Bug需要解決,也無法版本跟蹤管理。

  • 依賴的完整性無法校驗,基於域名的package名稱,域名變化或子路徑變化,都會導致無法正常下載依賴。我們在使用過程,發現還是有不少間接依賴包的名稱已失效了(不存在,或又fork成新的專案,舊的已不存維護更新)。

而Go官方對於此類問題的建議是把外部依賴的程式碼複製到你的原始碼庫中管理。把第三方程式碼引入自己的程式碼庫仍然是一種折中的辦法。

於是市面上就誕生了各種Go包管理工具,如glide,dev等。使用第三方包管理最大的好處是,每個專案都採用各自獨立的包,而且可以很好的控制包的版本,這在團隊開發中尤其重要。

而node的包管理我認為就相對好一些了,不管是npm還是yarn的包版本控制,都是棒棒噠。

其中還有一點需要注意的地方,就是glide的依賴是扁平化的,Glide 獲取的所有依賴都放置在專案頂級目錄下的vendor/中,而node中的依賴是集中存放在node_modules目錄下,鏈式依賴過多,並且是遞迴查詢。

型別系統

node是使用JavaScript這門動態語言、弱型別語言,好處自然不言而喻,比較靈活,忽略驗證資料的型別和真值判斷陷阱所帶來的額外負擔。但是JavaScript是在執行時進行解釋的,這可能會導致錯誤處理和除錯的問題。

go屬於靜態語言,也是強型別語言,雖然沒有動態語言靈活,但有助於資料完整,並可以在編譯時查詢型別錯誤。Go 被直接編譯成機器碼,這就是它速度的來源。使用編譯語言除錯是相當容易的,因為你可以在早期捕獲大量錯誤。

併發處理

node

node只適合IO密集型,它沒有提供太多的併發基元。唯一能同時執行的是I/O程式和定時器等,並不適合CPU密集型。

node執行機制:node執行機制是事件迴圈機制,每次從事件佇列中取出一個函式之類的,然後去執行它,如果過程中發生IO事件,比如利用fs模組寫一個檔案,或者去資料庫查詢資訊等,node就會將這個IO操作加入到一個執行緒池中去執行,事件迴圈在主執行緒繼續執行,當執行緒池中的事件執行完畢,就會將這個結果放入到主執行緒中。但是如果遇到計算密集型的任務,因為node是單執行緒,就會阻塞主執行緒直到該任務執行完畢才會往下執行,所以node不適合做CPU密集型。

d

go

go適合IO密集型同樣也適合CPU密集型,你可以在程式執行的任何階段,建立goruntine去實現併發,並且go提供了channel來實現協程間通訊,很贊有木有。

在作業系統提供的核心執行緒之上,Go搭建了一個特有的兩級執行緒模型。goroutine是實際併發執行的實體,每個實體之間是通過channel通訊來實現資料共享。關於go的併發及排程原理,戳這裡goroutine 排程原理

d

錯誤處理

Go 推薦在錯誤出現的地方捕獲它們,而不是像 Node 一樣在回撥中讓錯誤冒泡。

node的錯誤處理是錯誤前置,golang的錯誤處理是錯誤後置。

// Node 的錯誤處理
foo('bar', function(err, data) {
// 處理錯誤
}
複製程式碼
//Go 的錯誤處理
foo, err := bar()
if err != nil {
// 用 defer、 panic、 recover 或 log.fatal 等等處理錯誤.
}
複製程式碼

Go 中的錯誤處理分為錯誤和異常兩種,那麼錯誤和異常的區別是什麼呢?

錯誤和異常從語言機制上面講,就是error和panic的區別

  • 錯誤是指可能出現的地方出了問題,比如開啟一個檔案失敗,這種是在人們的意料之中;

  • 而異常指的是不應該出現問題的地方出現了問題,比如引用了空指標、下標越界、除數為0等,這種情況在人們的意料之外。

Golang中引入兩個內建函式panic和recover來觸發和終止異常處理流程,同時引入關鍵字defer來延遲執行defer後面的函式。

使用 Go 的錯誤處理時,應注意以下幾點

  • 失敗的原因只有一個時,不使用error

  • 沒有失敗時,不返回error

  • error應放在返回值型別列表的最後

  • 錯誤值統一定義,而不是跟著感覺走

  • 錯誤逐層往上拋時,層層都加日誌,便於定位錯誤

  • 當嘗試幾次可以避免失敗時,不要立即返回錯誤

  • 當上層函式不關心錯誤時,建議不返回error

Node中的錯誤處理主要分為以下三種情況

  • 非同步的函式裡,使用throw。使用者使用try...catch即可捕獲錯誤。

  • 非同步函式裡,更常用的方式是使用callback(err, result)的方式。

  • 在更復雜的場景裡,可以返回一個EventEmitter物件,代替使用callback。使用者可以監聽emitter物件的 error事件。 例如讀取一個資料流,我們可能會同時使用 req.on('data')、req.on('error')、req.on('timeout') 。

所以,使用throw還是callbacks、EventEmitter,取決於:

  • 該錯誤是操作錯誤還是編碼錯誤?

  • 該函式是同步還是非同步?

測試

在node中進行單元測試需要藉助第三方測試框架入mocha以及第三方斷言庫入should.js,相關文章 Node.js 單元測試:我要寫測試

使用Go的時候,我們喜歡測試框架的規範化。在Go裡,所有的測試包都是內建的。如果你需要寫一個新的測試套件,你必須做就是把(檔名)_test.go檔案加到你要測試的軟體的同一個包裡,它將會在你每次執行go test的時候執行。

提到單元測試,就不得不提的是測試的覆蓋率問題,在go中的測試覆蓋率有一點需要注意的地方,在go中計算單元測試覆蓋率是不包括沒有定義_test.go的模組的,只會統計已經定義了_test.go模組的單元測試覆蓋率。

部署

go的部署真的是非常簡單,將go build後生成的二進位制檔案直接丟到伺服器上去,然後執行這個程式,不需要任何語言環境,像java程式需要在伺服器安裝java,php需要安裝Apache,PHP等執行環境,go統統不需要,只需要一個linux系統就好,扔上去就可以了。

而node的部署的部署則需要在伺服器上安裝npm,或者藉助pm2,將專案程式碼拉倒伺服器,並跑起來。

社群

Node 和 Go 歲數相仿,社群也相對都比較完整了

作者的 Golang 系列博文:

剖析 golang 的25個關鍵字

Array、Slice、Map原理淺析

Go 與 Node 記憶體分配與垃圾回收

goroutine 排程原理

相關文章