(Photo From Go Brand Book)

已經有不少文章介紹如何用 modules 引入外部 package,但一般公司更常見的狀況是專案內需要引入自己寫的 internal package,本文講解 go modules 基本觀念以及利用實例說明如何安排專案結構。

Update:

  • 2018.10.17 新增專文介紹: 如何透過 Go Modules 引入 insecure private 專案
  • 2018.10.05 vscode-go v0.6.90 正式支援 modules
  • 2018.09.27 新增 vscode-go beta資訊,已可支援 autocomplete 以及 definition
  • 2018.09.21 新增 gocode autocomplete hotfix
  • 2018.09.12 更新 vscode-go 對於 modules 相關支援現況

modules 重點摘要

  • 主要目的是解除 $GOPATH 的依賴,同時開發多專案時無需頻繁切換 vendor 目錄,而 dep 僅為過渡型解決方案。
  • 目前屬於實驗功能,可用環境變數 GO111MODULE 控制行為:
    • off: go command 不使用 modules 功能,而是沿用舊有的 GOPATH 模式
    • on: 強制使用 modules 功能,只根據 go.mod 下載 dependency 而完全忽略 GOPATH 以及 vendor 目錄
    • auto: Golang 1.11 預設值,go command 根據當前工作目錄狀態決定是否啟用 modules 功能,滿足任一條件時才啟動此功能:
      • 當前目錄位於 GOPATH/src 之外並且包含 go.mod 文件
      • 當前目錄位於包含 go.mod 文件的目錄下
  • modules 下載的 package 預設放在 $GOPATH/pkg 目錄下,允許同 package 多種版本並存。
  • go build, go get, go test 等指令都會影響 go.mod,請多留意。

modules 基本用法

專案結構

myproj
├── main.go
└── pkg
    └── myapi
        └── data
            └── api.go

創建 modules

本例用 Gin 框架來實作兩個 restful API,handler實作分別屬於 main 以及 pkg/myapi/data 兩個 internal package。

進入 myproj 目錄,並用內建指令建立 modules

$ go mod init myproj
go: creating new go.mod: module myproj

此時自動產生 go.mod 檔案,內容只有一行

module myproj

我們來看看兩支go檔內容:

// main.go

package main

import (
    dataapi "myproj/pkg/myapi/data"
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    router.GET("/health", GetHealthHandler)
    router.GET("/health-dataapi", dataapi.GetDataAPIHealthHandler)

    s := &http.Server{
        Addr:    ":8000",
        Handler: router,
    }
    s.ListenAndServe()
}

// GetHealthHandler - GET /health to expose service health
func GetHealthHandler(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "code":    0,
        "message": "Service is alive!",
    })
}

// pkg/myapi/data/api.go
package data

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

// GetDataAPIHealthHandler GET /health-dataapi to expose heathy check result of data API
func GetDataAPIHealthHandler(c *gin.Context) {
    // do something to check heathy of data API
    c.JSON(http.StatusOK, gin.H{
        "code":    0,
        "message": "Data API is alive",
    })
}

可以發現 main.go 裡面宣告使用 internal package 的方法跟以前很不一樣,由於 go.mod會掃描同工作目錄下所有 package 並且變更引入方法,必須將 myproj 當成路徑的前綴,也就是需要寫成 import myproj/pkg/myapi/data/,以往 GOPATH/dep 模式允許的 import ./pkg/myapi/data 已經失效,詳情可見此 issue

此時用老朋友 go build 創建執行檔,並輸出到 bin/main

$ go build -o bin/main main.go

可發現此時 go.mod 內容被拓展為:

module myproj

require (
    github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
    github.com/gin-gonic/gin v1.3.0
    github.com/golang/protobuf v1.2.0 // indirect
    github.com/mattn/go-isatty v0.0.4 // indirect
    github.com/ugorji/go/codec v0.0.0-20180831062425-e253f1f20942 // indirect
    gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
    gopkg.in/yaml.v2 v2.2.1 // indirect
)

go module 拉取 package 的原則是先拉最新的 release tag,若無tag則拉最新的commit,詳見 Modules官方介紹。 go 會自動生成一個 go.sum檔案記錄整個 dependency tree:

github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/ugorji/go/codec v0.0.0-20180831062425-e253f1f20942 h1:CZORS/4d6i+5FKSAtbRIjlElV2BAFYv/bokcaEVUimQ=
github.com/ugorji/go/codec v0.0.0-20180831062425-e253f1f20942/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

有寫過 node 的人應該發現 go.mod/go.sum 的關係跟 package.json/package-lock.json 類似,前者定義 dependency root,後者將關係展開。

最後執行 bin/main 可看到 Gin 貼心列出 handler 從屬的 package。

$ bin/main
[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.

[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /health                   --> main.GetHealthHandler (3 handlers)
[GIN-debug] GET    /health-dataapi           --> myproj/pkg/myapi/data.GetDataAPIHealthHandler (3 handlers)
Listening port 8080

到這邊一個 go module 就完成了,以往需要將 vendor 目錄一起提交到 git 以免 CI/CD 流程拉到錯誤的外部 package,有了 modules 加上 build cache 以後,在 build server 上面跑起來體感速度比 1.10 時代還快。

如果你習慣專案裡要有 vendor 目錄,可以執行以下指令產生 vendor 目錄:

$ go mod vendor
go: finding golang.org/x/sys/unix latest
go: finding golang.org/x/sys latest
go: downloading golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789
go: finding github.com/modern-go/concurrent latest

modules 大致使用方法講到這邊,由於這是個實驗性的功能,不建議大規模把公司的專案搬過去,畢竟軟體工程界的鐵則就是不要用 .0 的版本 :p

VS code 開發套件 issues 列表

Golang 1.10 使用dep以後不強制要求 GOPATH 設定 vendor 目錄,依本文的專案結構會發現 gocode 無法正確抓到 internal package,解決方法參考本文 轉換 nsf/gocode -> mdempsky/gocode 套件。可以追蹤下列連結了解最新支援進度:

參考資料

 

Facebook Comments