gvm + go mod

Rain Wu
Golang 筆記
Published in
18 min readMay 29, 2019

最近處理的專案中,用到了不少 Golang 開發的工具,都是蠻大的項目,激起了我潛意識中對於版本及套件管理的恐慌感。雖說我現在自己在寫的東西規模都還小,沒有那些問題,但也差不多快到不好好管理就要毀滅的臨界點了,於是抽空來研究了下 Golang 一些比較近期的管理工具。

gvm

go version manager,一個讓我們能統一管理並切換多個不同版本 Golang 的好工具,Repo 在這裡如果運氣好的話應該照著裝不會出什麼大問題,但逛了一下他的 Issue 和各論壇的討論熱度,就知道肯定大家運氣都不太好。

下載完之後,先下個指令確認他有正確安裝

$ gvm listgvm gos (installed)

go1.12
go1.4
system

我的是已經裝了 1.4 和 1.12 兩個版本,只要透過 gvm install 就能安裝了

$ gvm install go1.6
Installing go1.6…
* Compiling…
ERROR: Failed to compile. Check the logs at /home/user/.gvm/logs/go-go1.6-compile.log
$ cat /home/user/.gvm/logs/go-go1.6-compile.log
cmd/dist
ERROR: Cannot find /home/user/go1.4/bin/go.
Set $GOROOT_BOOTSTRAP to a working Go tree >= Go 1.4.

如果一個 Golang 版本管理工具連個 Golang 都裝不了,那他還有什麼存在意義呢 OAO?

回頭看看就會發現,诶不對阿為什麼我裝了 1.12 還要裝 1.4?這就是重點了, Golang 在 1.5 版之後捨棄 C 直接用 Golang 來進行 Compile,如果沒有任何 Golang 版本那要怎麼 Compile 呢?所以我們必須先裝個 1.4 版本的。

$ gvm install go1.4
Installing go1.4...
* Compiling...
ERROR: Failed to compile. Check the logs at /Users/cage/.gvm/logs/go-go1.4-compile.log
ERROR: Failed to use installed version

心態崩潰,1.4 版裝不了後面的版本都不用裝了,去找了一下後發現了這則 commit,描述上面提到須由 Golang compile 的問題,以及在下載 go1.4 需加上 -B 這個參數:

gvm install go1.4 -B
Installing go1.4 from binary source

看似完成了,接著記得啟用他,這樣才會自動套上 go env,這功能真的棒,省掉 env 出問題需要花費的心思,但貌似有時會被清除,需要重新指定版本,具體清除時機我也不是很清楚 @_@ 一直重新指定有點麻煩就是了,之後再想辦法解決。

我們也可以透過剛剛的 gvm list 來確認當前使用的版本,當前使用版本前方會多個 => 記號來標註。

$ gvm use go1.4
Now using version go1.4
$ gvm listgvm gos (installed)

go1.12
=> go1.4
system

看起來...終於能安裝近期的新版本了吧?可能沒那麼簡單,如果和我一樣在資源受限的機器上操作的話,可能就有內存不足的問題。通常會導致 signal: killed 相關的錯誤資訊,因為近幾個版本的 Golang 編譯大約需要 1G 左右的內存,遇到這問題的話用些 swap memory 的技巧或是暫時拉高內存使用限制就行了。

到這邊應該已經能把 gvm 設定完成了,折騰了許久,gvm 在使用上還是有不少障礙,但確實解決了多個 Golang 版本管理以及 go env 一更動就天下大亂的問題,所以還是蠻建議使用他的。

go mod

解決了 Golang 版本管理的問題之後,輪到套件管理的問題了,目前 Golang 的套件複雜程度相對於其他語言來說應該算是簡單許多,不過學一下新的工具還是十分有幫助的。

在 go mod 誕生之前,多半套件管理都是使用 go dep, go vendor 這類工具,而工具演變上之所以會有後浪暴打前浪的趨勢,肯定是現存工具有些棘手問題點遲遲沒解決,所以才催生出了新的工具。

前幾項工具我自己沒使用太多,沒什麼經驗能分享,但推薦這位作者所寫關於 go depgo vendor / go mod 的文章。至於 go mod 誕生的緣由,可以看這個知乎上的討論串,內文附上了很多第一手資訊連結,雖說有點八卦但花個幾分鐘了解多些資訊也不會虧到哪裡去。

好了回到正題,go mod 是 go1.11 才開始導入,所以無論如何都得要裝個 go1.11 或是更新的版本,現在 go1.12 正式發布了,我自己是用 1.12 版。一開始記得要設置 GO111MODULE=on,雖說我自己在操作時就算沒設置也是能用,但如果你遇到問題記得檢查一下這個環境變數。

直接用 go mod 指令測試是否正常運作,你應該能看到和我一樣的輸出:

$ go mod
Go mod provides access to operations on modules.
Note that support for modules is built into all the go commands,
not just ‘go mod’. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using ‘go get’.
See ‘go help modules’ for an overview of module functionality.
Usage: go mod <command> [arguments]The commands are: download download modules to local cache
edit edit go.mod from tools or scripts
graph print module requirement graph
init initialize new module in current directory
tidy add missing and remove unused modules
vendor make vendored copy of dependencies
verify verify dependencies have expected content
why explain why packages or modules are needed
Use “go help mod <command>” for more information about a command.

到這一步應該沒什麼問題,但如果執行 go mod 的子命令呢?先從初始化開始吧,看到 init 指令的說明,是寫著會在當前指令中初始化一個新的 mod,但我實際執行時卻發生問題:

$ go mod init
go: cannot determine module path for source directory /home/ubuntu/go/mod-demo (outside GOPATH, no import comments)

噢...如果你也遇到類似問題的話,原因就如同錯誤訊息,他沒辦法定位 GOPATH 外的路徑,但這也是他相較於以往 Golang 套件管理工具的優勢之一,就是它可以讓我們不必拘束於 GOPATH 中開發,現在我們直接附上指定的檔案路徑。

$ go mod init /go/mod-demo
go: creating new go.mod: module /go/mod-demo
$ ls
go.mod
$ cat go.mod
module /go/mod-demo
go 1.12

附上路徑後就能正確初始化了,可以發現他自動幫我們創建了一個 go.mod,裡面很乾淨的只有我們目前開發的 module 名稱和 go 版本,現在常試來寫點東西,就一個簡單的 Hello World 就好。

$ cat main.go
package main
import ( “fmt”
)
func main() { fmt.Println(“Hello World!”)
}

如果你沒寫過 Golang 的話,那你有耐心持續看這篇文章到這裡我也是蠻佩服的!呃不對,我是說 Golang 的核心精神是簡潔,沒用到的變數、package都不能留,尾端空白也要注意,建議能用個 gofmt 或 golint 的工具來輔助。現在來 build 看看會不會有什麼事情發生~

$ go build$ ./mod-demo
Hello World!
$ ls
go.mod main.go mod-demo
$ cat go.mod
module /go/mod-demo
go 1.12

哦~好像沒什麼特別的事,一如往常,如果你有遇到錯誤訊息類似

go: disabling cache (/.cache/go-build) due to initialization failure: permission denied

那麼原因主要出在於 Golang 預設使用的 cache,他假設你會有讀寫權限,但你沒有的話就會報錯,解決方法是把一個特定的環境變數修改一下, XDG_CACHE_HOME=/tmp/.cache,這樣就行了。更詳細可以參考這則提問以及 Source code 處理方式

看起來對於 Golang 內建 package 的話,go mod 是不會管他們的,那現在來試試看標準庫以外的 package:

$ cat main.go
package main
import (

“fmt”
“github.com/gocolly/colly”
)
func main() { c := colly.NewCollector()
c.OnRequest(func(r *colly.Request) {
fmt.Println(“Visiting”, r.URL)
})
c.Visit(“https://coinmarketcap.com/all/views/all/")
}
$ go build
go: finding github.com/temoto/robotstxt latest
go: finding github.com/saintfish/chardet latest
go: finding google.golang.org/appengine/urlfetch latest
go: finding golang.org/x/net/html latest
go: finding golang.org/x/net/html/charset latest
go: finding golang.org/x/net latest
go: finding github.com/antchfx/xpath latest
$ ls
go.mod go.sum main.go mod-demo

沒意外的話,只要下了 go build|test|install 之類的指令,go mod 就會幫忙找尋所需套件的最新版本,並下載到 GOPATH/pkg/mod,我之前有用過 gocolly 所以比較迅速,如果你是第一次用的話可能要等些下載時間。

我們當然也可以指定舊版本套件,特別是在各套件相依而版本間又不相容,搞得一整個盤根錯節的時候,只是我還沒遇到這問題所以就先不提了,如果需要的話可以參考這篇文章,他提供的做法蠻簡潔的。

繼續來看看 build 完之後的資料夾情況,是多了一個 go.sum 檔案,看看它裡面有什麼:

$ cat go.sum
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antchfx/htmlquery v1.0.0 h1:O5IXz8fZF3B3MW+B33MZWbTHBlYmcfw0BAxgErHuaMA=
github.com/antchfx/htmlquery v1.0.0/go.mod h1:MS9yksVSQXls00iXkiMqXr0J+umL/AmxXKuP28SUJM8=
github.com/antchfx/xmlquery v1.0.0 h1:YuEPqexGG2opZKNc9JU3Zw6zFXwC47wNcy6/F8oKsrM=
github.com/antchfx/xmlquery v1.0.0/go.mod h1:/+CnyD/DzHRnv2eRxrVbieRU/FIF6N0C+7oTtyUtCKk=
github.com/antchfx/xpath v0.0.0–20190319080838-ce1d48779e67 h1:uj4UuiIs53RhHSySIupR1TEIouckjSfnljF3QbN1yh0=
github.com/antchfx/xpath v0.0.0–20190319080838-ce1d48779e67/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
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/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/saintfish/chardet v0.0.0–20120816061221–3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
github.com/saintfish/chardet v0.0.0–20120816061221–3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/temoto/robotstxt v0.0.0–20180810133444–97ee4a9ee6ea h1:hH8P1IiDpzRU6ZDbDh/RDnVuezi2oOXJpApa06M0zyI=
github.com/temoto/robotstxt v0.0.0–20180810133444–97ee4a9ee6ea/go.mod h1:aOux3gHPCftJ3KHq6Pz/AlDjYJ7Y+yKfm1gU/3B0u04=
golang.org/x/crypto v0.0.0–20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0–20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0–20180724234803–3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0–20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0–20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0–20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/sys v0.0.0–20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

看起來是紀錄了所有相依套件還有他的來源、版本資訊,以及校驗用的 Hash 值,這個 go.sum 並不是 go file,簡單來說,他像是一個紀錄套件之間直接和間接關係,並能有類似 VCS 功能保存過往套件使用資訊的文檔。目的是在於確保每次集成套件時全盤掌控每個部分,包括回溯到先前版本的情況。網路上討論這檔案的文章並不多,有興趣的話可以參考這裡

$ cat go.modmodule /go/mod-demogo 1.12require (
github.com/PuerkitoBio/goquery v1.5.0 // indirect
github.com/antchfx/htmlquery v1.0.0 // indirect
github.com/antchfx/xmlquery v1.0.0 // indirect
github.com/antchfx/xpath v0.0.0–20190319080838-ce1d48779e67 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gocolly/colly v1.2.0
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/saintfish/chardet v0.0.0–20120816061221–3af4cd4741ca // indirect
github.com/temoto/robotstxt v0.0.0–20180810133444–97ee4a9ee6ea // indirect
golang.org/x/net v0.0.0–20190522155817-f3200d17e092 // indirect
google.golang.org/appengine v1.6.0 // indirect
)

go.mod 同樣有所變動,require 裡面包了許多新東西,基本上套件下載下來還是放在 GOPATH 下的,而這裡就是把相對於 GOPATH 的路徑給標上,並附上版本資訊。

你可能會好奇,我們不是只用了 gocolly 一個 package 嗎?哪來那麼多個,可以發現除了 gocolly 外其他的 package 資訊尾端都有個 // indirect 註記,那表示間接依賴關係。也許我們沒有直接 import,但我們所用的其他套件有需要,因此也被記錄下來了。

就上述功能來說,go.mod 和 go.sum 肯定是要加入版控的,以確保在不同版本都能成功的 build。

結語

我自認為我在入手這兩個工具的過程算是比較坎坷的,踩的雷還真的不少,所幸還是都有順利解決問題了,如果你正好想點亮一些使用 Golang 專案管理工具的技能,希望這篇筆記有幫助到你。

--

--

Rain Wu
Golang 筆記

A software engineer specializing in distributed systems and cloud services, desire to realize various imaginations of future life through technology.