一名 Node.js 工程師的 Go 學習之旅

一名 Node.js 工程師的 Go 學習之旅


近幾年, Kubernetes 一直是容器編排和管理平臺的首選。對於我來說,也很想搞清楚它的底層是如何工作的,所以我決定來學一學 Go 這門語言。

在這邊文章中,我會基於我的經驗,並從一個 Node.js 工程師的視角出發。因此我會特別關注:

  • 依賴管理
  • 如何處理異步操作
  • 那就讓我們開始吧 :smile:

什麼是 Go ?

Go 是一個開源的編程語言,它能讓構造簡單、可靠且高效的軟件變得容易. - golang.org

Go 由 Google 的 Robert Griesemer, Rob Pike, 和 Ken Thompson 於 2009 年發佈。它是一款靜態類型的編譯語言,擁有垃圾收集機制,基於 CSP 併發模型來處理異步操作。 Go 還有類 C 的語法:

package main
import "fmt"
func main() {

fmt.Println("hello world")
}

安裝 Go: 官方指導鏈接: https://golang.org/doc/install.

Go 的依賴管理

如果打算寫大量的 JavaScript 代碼,首要問題就是依賴管理問題,Go 是怎麼處理的呢?有兩個辦法:

  • go get
  • dep
  • 用 npm 的術語來說,你可以把他們看作是:在你需要使用 npm install -g 的時候,使用 go get,然後使用 dep 來管理不同項目的依賴。

要安裝 dep,可以通過 go get 來安裝,使用如下命令:

go get -u github.com/golang/dep/cmd/dep


然而,使用 go get 有一個缺點 —— go get 並不處理版本,它僅僅是獲取 Github 倉庫的最新版本。這就是為什麼推薦大家安裝並使用 dep。如果是 Mac 系統,安裝 dep 也可以通過如下命令:

brew install dep
brew upgrade dep

(如果是其他操作系統,安裝請參見: https://golang.org/doc/install)

一旦安裝了 dep,你就可以使用 dep init 來初始化項目,就好像使用 npm init 初始化 nodejs 項目一樣。

開發Go項目之前,你需要花點時間設置好GOPATH環境變量。—— 官方指導鏈接

dep 會像 npm 一樣,創建一個 Node.js 項目中類似 package.json 的文件來描述工程 —— Gopkg.toml。類似 package-lock.json,也會有一個 Gopkg.lock 文件。不同於 nodejs 項目將依賴放入 node modules 文件夾中,dep 將依賴放入一個叫作 vendor 的文件夾中。

要添加依賴,你只需要運行 dep ensure -add github.com/pkg/errors 命令。運行結束後,這個依賴就會出現在 lock 和 toml 文件中:

[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"

Go處理異步操作

當用 JavaScript 寫異步代碼時,我們會用到一些庫或者語言特性,比如:

  • async 庫
  • Promises
  • 或者異步函數
  • 有了它們,我們可以很輕易的從文件系統中讀取文件:
const fs = require('fs')
const async = require('async')
const filesToRead = [
'file1',
'file2'
]
async.map(filesToRead, (filePath, callback) => {
fs.readFile(filePath, 'utf-8', callback)
}, (err, results) => {
if (err) {
return console.log(err)
}
console.log(results)
})

再讓我們來看看 Go 是怎麼實現的:

package main
import (
"fmt"
"io/ioutil"
)
func main() {
datFile1, errFile1 := ioutil.ReadFile("file1")
if errFile1 != nil {
panic(errFile1)
}
datFile2, errFile2 := ioutil.ReadFile("file2")
if errFile2 != nil {
panic(errFile2)
}
fmt.Print(string(datFile1), string(datFile2))
}

我們來一行行看看上面代碼是怎麼工作的:

  • import —— 有了 import 關鍵字,你可以引入項目依賴的包文件,就像 Node.js 中的 require
  • func main —— 應用程序的入口
  • ioutil.ReadFile —— 該函數嘗試去讀取文件,並由兩個返回值:
  • datFile1 如果讀操作成功,
  • errFile1 如果讀取過程中有錯
  • 你可以在這裡處理錯誤,或者直接讓程序崩潰
  • fmt.Print 僅僅是打印結果到標準輸出
  • 上面的例子是可以正常運行的,但是讀取文件是一個接一個讀取。—— 讓我們來稍加改進,將它異步化吧!

Go有一個叫作 goroutines 的概念來處理多線程。一個 goroutine 是一個輕量級的線程,它由 Go runtime 來管理。goroutine 使得你可以併發地跑Go的函數。

我最終使用 errgroup 包來管理或者說同步 goroutines。這個包提供同步機制,錯誤傳播,以及對同一個由一組 goroutines 子任務組成的公共任務提供上下文取消機制。

有了 errgroup,我們可以重寫讀文件的代碼片段,併發地執行:

package main
import (
"context"
"fmt"

"io/ioutil"
"os"
"golang.org/x/sync/errgroup"
)
func readFiles(ctx context.Context, files []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(files))
for i, file := range files {
i, file := i, file
g.Go(func() error {
data, err := ioutil.ReadFile(file)
if err == nil {
results[i] = string(data)
}
return err
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
func main() {
var files = []string{
"file1",
"file2",
}
results, err := readFiles(context.Background(), files)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
for _, result := range results {
fmt.Println(result)
}
}

用 Go 來構建一條 REST API

在 Node.js 中,當談及到選擇一套框架來寫 HTTP 服務的時候,我們有一堆的選擇。Go 在這方面也不例外。Google 一下後,我選擇 Gin 來開始。

Gin 的接口類似於 Express 或者 Koa,包含中間件支持,JSON 校驗以及渲染:

package main
import "github.com/gin-gonic/gin"
func main() {
// 創建默認不帶任何中間件的路由
r := gin.New()
// 默認gin的輸出為標準輸出
r.Use(gin.Logger())
// Recovery中間件從異常中恢復,並回復500
r.Use(gin.Recovery())
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
// 監聽 0.0.0.0:8080
r.Run(":8080")
}

這就是我目前掌握的 —— 還沒有生產經驗。如果你這篇博客對你有幫助,並且你想學到更多關於 Go 的東西,就跟我聯繫吧,我會持續分享我的 Go 學習之旅。


分享到:


相關文章: