Golang入門-Golang包管理

言淦發表於2020-02-01

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也要配置

Golang入門-Golang包管理

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

參考:golang.org/cmd/go/#hdr…

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

複製程式碼

推薦閱讀

Go語言包管理簡史
初窺Go module
Go modules:版本是如何選擇的?
談談go.sum

相關文章