Golang的包管理一直是廣大開發者吐槽的點之一。
Go 包管理簡史
Golang的包管理分為三個階段,version < 1.11、 1.11 <= version < 1.13、 version >= 1.13。
version < 1.11
在這個階段,Golang的包管理存在以下不足:
- 必須設定GOPATH環境變數,且原始碼必須存放在GOPATH下
- 拉取外部依賴包時,總是拉取最新的版本,無法指定需要的版本
之所以設定GOPATH環境變數有兩個原因:
- 它規定了
go get
命令下載的依賴包的儲存位置($GOPATH/src) - 通過設定GOPATH,可以方便Golang計算出import的路徑
另外,由於無法指定依賴包的版本,因此容易導致“本地測試OK,但線上部署失敗”的問題。這樣的問題是廣大開發者無法忍受的,所以,各種包管理工具開始湧現出來,典型的有dep,glide等,這裡不再贅述。
1.11 <= version < 1.13
這個階段預設使用的還是GOPATH的管理方式,但是開始支援Go Module
的管理方式。
Go Module解決了上述的階段存在的不足:
1.它不再需要GOPATH,即你的專案程式碼可以隨意存放
2.它通過go.mod + go.sum解決依賴包的版本問題(後面會講到)
如果需要遷移到Go Module,需要設定以下環境變數
vim ~/.bash_profile
export GO111MODULE=on
複製程式碼
version >= 1.13
從這個階段開始,Golang的包管理預設使用的是Go Module。
使用GOPATH進行包管理
注:為了完整性,這裡嘗試使用go 1.11復現之前使用GOPATH進行包管理的情況。
1.下拉docker映象
$ docker pull ubuntu:16.04
$ docker run -itd --name golang-lab ubuntu:16.04 /bin/bash
$ apt-get update && apt-get install wget
複製程式碼
2.安裝go 1.11
$ wget https://dl.google.com/go/go1.11.10.linux-amd64.tar.gz
$ tar -zxvf go1.11.10.linux-amd64.tar.gz
$ go/bin/go version
go version go1.11.10 linux/amd64
複製程式碼
3.新建專案
3.1 這裡我們假定/home/go-projects
為我們的工作區
3.2 新建bin目錄用於存放可執行檔案; 新建pkg目錄用於存放靜態連結庫檔案; 新建src目錄用於存放的我們原始碼檔案, 一般我們寫的程式碼都會放到這個目錄下。
3.3 git.own.com
名稱可自定義,這裡只是個人程式設計習慣,表示這裡存放的都是個人專案
$ mkdir /home/go-projects
$ cd /home/go-projects && mkdir src && mkdir pkg && mkdir bin
$ cd src && mkdir git.own.com && cd git.own.com
$ mkdir gopath-lab && cd gopath-lab && touch main.go
複製程式碼
4.目錄樹
root@ebca4ae962aa:/home/go-projects# tree -L 4
.
|-- bin
|-- pkg
`-- src
`-- git.own.com
`-- gopath-lab
`-- main.go
複製程式碼
5.設定環境變數
- GOPATH:工作區路徑,存放原始碼。
- GOBIN:當使用go install xx.go 時, 生成的可執行檔案就會放在此目錄
- GOROOT:Go的安裝位置,用於尋找標準庫,這裡是/home/go
$ vim ~/.bashrc
export PATH=$PATH:/home/go/bin
export GOPATH=/home/go-projects
export GOBIN=/home/go-projects/bin
export GOROOT=/home/go
複製程式碼
如果沒有設定GOBIN,會報錯
$ go install main.go
go install: no install location for .go files listed on command line (GOBIN not set)
複製程式碼
6.main.go 程式碼如下:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
複製程式碼
可以看到,直接go run
並不能自動下載依賴
$ go run main.go
main.go:3:8: cannot find package "github.com/gin-gonic/gin" in any of:
/home/go/src/github.com/gin-gonic/gin (from $GOROOT)
/home/go-projects/src/github.com/gin-gonic/gin (from $GOPATH)
複製程式碼
7.手動下載並測試
# 居然奇蹟般下載成功了,一般這個時候需要設定代理
$ go get -v github.com/gin-gonic/gin
# 可以看到,原始碼已經下載到src目錄了
$ ls /home/go-projects/src/
git.own.com github.com golang.org gopkg.in
# 再次執行,執行成功
$ go run main.go
[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 /ping --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
複製程式碼
使用Go Module進行包管理
本節翻譯自《Using Go Modules》
Module 是一系列依賴包的集合,通過go mod init xxx
可初始化一份空的go.mod和go.sum,這兩份檔案存放於專案的根路徑下。
對於go.mod,它不僅儲存了這些依賴包的路徑及其版本,同時也指定了import的根路徑,對於go.sum,它存放了依賴包內容的預期校驗和,保證前一次下載的程式碼和現在下載的程式碼是一致的。
配置代理
由於Golang大部分依賴包都在國外,直接下載非常緩慢,在沒有Go Module的時候,需要自己配置代理,比如socks;但是有了Go Module,就可通過設定環境變數來配置代理了,具體參考:goproxy.io/zh/。
配置時有幾個注意點:
1.如果你有私有倉庫和公共倉庫,則需要加上direct
引數,並配置GOPRIVATE
(針對Go1.13)
# 有了direct,GOPRIVATE指定的倉庫不會使用代理
go env -w GOPROXY=https://goproxy.io,direct
# 設定不走代理的私有倉庫,多個用逗號相隔
go env -w GOPRIVATE=*.corp.example.com
複製程式碼
2.如果你使用的是Golang IDE,則注意該IDE也要配置
3.如果你的~/.bash_profile或~./bashrc 檔案存在GO111MODULE等環境變數,則go env 寫入時會衝突
warning: go env -w GOPROXY=... does not override conflicting OS environment variable
初始化專案
1.新建資料夾
mkdir go-module-lab && cd go-module-lab
2.初始化Go Module專案,git.own.com/go-module是自定義的
go mod init git.own.com/go-module
3.檢視go.mod
module git.own.com/go-module
go 1.13
複製程式碼
新增程式碼測試
1.自定義庫
mkdir hello && touch hello/hello.go
hello.go 內容
package hello
func Hello() string {
return "Hello, world."
}
複製程式碼
2.新建main.go測試,內容如下
package main
import (
"fmt"
// 前面提過,go.mod 指定了import時的根路徑
"git.own.com/go-module/hello"
)
func main() {
fmt.Println(hello.Hello())
}
複製程式碼
新增外部依賴
1.更新hello.go檔案,引入rsc.io/quote
依賴
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
複製程式碼
2.執行go run main.go
,會自動下載依賴
➜ go-module-lab go run main.go
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
Hello, world.
複製程式碼
3.檢視go.mod
module git.own.com/go-module
go 1.13
require rsc.io/quote v1.5.2
複製程式碼
可以看到,使用Go Module的包管理方式,Golang會自動幫我們處理包的依賴關係,並把缺失的包新增到go.mod,並使用rsc.io/quote
的最新版本。(這裡的最新版本應理解為最新並打了tag的版本,如果沒有打tag,則會使用一種pseudo-version
的方式標識,下文會說到)
4.藉助go list命令檢視所有依賴
$ go list -m all
git.own.com/go-module
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
複製程式碼
補充:
pseudo-versions(偽版本)
一般情況下,go.mod使用語義化版本
來標誌依賴包的版本號,比如v1.0.0、v1.0.1。
它包含三個部分:
- 主版本號:當你做了不相容的 API 修改,比如v1.5.2的1
- 次版本號:當你做了向下相容的功能性新增,比如v1.5.2的5
- 修訂號:當你做了向下相容的問題修正,比如1.5.2的2
語義化版本規定,同一個主版本號的必須向下相容,比如v1.5.2必須向下相容v1.1.0;如果程式碼不相容,則必須使用新的版本號。
但是語義化版本是基於專案有打tag的情況下,如果一些專案沒有打tag,則Golang會使用一種pseudo-version
來標識,類似v0.0.0-yyyymmddhhmmss-abcdefabcdef
的形式。
其中,yyyymmddhhmmss使用的是UTC時間,abcdefabcdef對應的是你這次commit的雜湊值(前12位),
對於字首v0.0.0,則有三種情況:
1.當你的專案一個tag都沒有的時候,形式為v0.0.0-yyyymmddhhmmss-abcdefabcdef
2.當你專案最近打的tag的名稱為vX.Y.Z-pre的時候,形式為vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef
3.當你的專案最近打的tag的名稱是vX.Y.Z的時候,形式為vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef
go.sum
之所以有go.sum檔案,是因為單純地通過語義化版本(v1.5.2)無法確定每次通過v1.5.2標籤下載的都是同一份程式碼。
比如釋出者在 GitHub 上給自己的專案打上 v1.5.2 的tag之後,依舊可以刪掉這個tag ,提交不同的內容後再重新打個 1.5.2 的 tag。
為了確定是否是同一份程式碼,go.sum存放了特定模組版本的內容的預期校驗和,如果該程式碼有改動,則預期校驗和不匹配,就會導致編譯錯誤。
verifying xxx/base@v1.3.0: checksum mismatch
downloaded: h1:T2eK+D0jzzeu4+S+oP9KvGgovPnl4FjxYShqdNSPrjc=
go.sum: h1:Crwm2FliMjZ3BABjnydOpoJiFPaKcod/zYNOtcB9Xkw=
複製程式碼
更新外部依賴
更新次版本號
更新次版本號比較簡單,直接使用go get即可,比如更新golang.org/x/text
go get golang.org/x/text
通過檢視go.mod的變化,我們可以看到golang.org/x/text
的版本號由v0.0.0-20170915032832-14c0d48ead0c升級到v0.3.2。(indirect表明該依賴包在原始碼中沒有用到,是間接依賴的)
module git.own.com/go-module
go 1.13
require (
golang.org/x/text v0.3.2 // indirect
rsc.io/quote v1.5.2
)
複製程式碼
除此之外,我們還可以更新到特定版本,在此之前,我們先看看該模組有哪些可用版本(以rsc.io/quote為例)
$ go list -m -versions rsc.io/quote
rsc.io/quote v1.0.0 v1.1.0 v1.2.0 v1.2.1 v1.3.0 v1.4.0 v1.5.0 v1.5.1 v1.5.2 v1.5.3-pre1
複製程式碼
更新到特定版本:
go get rsc.io/quote@v1.4.0
如果想要使用特定的分支,只需要把版本號換成分支名即可(如果分支名包含特定符號,如"/",可用雙引號將分支名括起來):
go get rsc.io/quote@dev
更新主版本號
如果需要更新主版本號,需要在程式碼中手動指定,因為不同主版本號相當於一個新的依賴(庫)。
1.新增新函式
package hello
import (
"rsc.io/quote"
quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
return quote.Hello()
}
func Proverb() string {
return quoteV3.Concurrency()
}
複製程式碼
2.自動下載依賴
package main
import (
"fmt"
"git.own.com/go-module/hello"
)
func main() {
fmt.Println(hello.Hello())
fmt.Println("proverb", hello.Proverb())
}
複製程式碼
3.檢視go.mod
module git.own.com/go-module
go 1.13
require (
golang.org/x/text v0.3.2 // indirect
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1 // indirect
)
複製程式碼
從上面可以看出,Go Module每一個主版本號使用不同的路徑表示,如v1,v2,v3;另外,Golang允許同時存在多個主版本號,因為路徑不同,相當於是一個新的庫,這樣做的目的是保持增量遷移。
比如我一開始使用rsc.io/quote
,後面有改動,且與之前不相容,這是我就可以使用新的主版本號,比如rsc.io/quote/v3
,但是Hello這個函式暫時還不能遷移到V3版本,這是多版本的作用就凸顯出來了
刪除多餘依賴
當過了一段時間,我們已經把把rsc.io/quote
的程式碼全部遷移到新版本rsc.io/quote/v3
, 類似下面的程式碼
package hello
import (
quoteV3 "rsc.io/quote/v3"
)
func Hello() string {
return quoteV3.HelloV3()
}
func Proverb() string {
return quoteV3.Concurrency()
}
複製程式碼
這時之前的go.mod裡面的rsc.io/quote
是多餘的,我們可以通過go mod tidy
刪除多餘的rsc.io/quote
$ go mod tidy
$ cat go.mod
module git.own.com/go-module
go 1.13
require (
golang.org/x/text v0.3.2 // indirect
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1 // indirect
)
複製程式碼
總結
1.go mod init: 初始化一個Go Module專案,同時生成go.mod和go.sum檔案
2.go build/go test/go run: 會自動下載依賴,並更新go.mod和go.sum檔案
3.go list -m all:列印目前的所有依賴包
4.go get:手動下載依賴包,或者更改依賴包版本
5.go mod tidy:增加缺失的依賴,刪除沒有用到的依賴
其他命令
go env
配置一些環境變數。
# 環境變數說明文件
go help environment
# 環境變數配置檔案路徑
$ go env GOENV
/Users/xxx/Library/Application Support/go/env
# 列出所有環境變數
go env
# 列出所有環境變數(以json格式)
go env -json
# 修改某個環境變數
go env -w GOPROXY=https://goproxy.io,direct
# 重置某個變數
go env -u GOPROXY
複製程式碼