Go 1.13版本之后新的包管理器Modules趋于成熟,目前越来越多的开源项目已经支持Go Modules,典型的如etcd。Go具有相当长的包管理工具变迁史,各种包管理工具层出不穷,究其原因,还是官方没有实现足够好用包管理工具。本文不对部分基础知识做详解,主要重点是Go Modules,本文来源:[https://roberto.selbach.ca/in...]()
GOPATH的缺陷
几乎所有的包管理工具在Go 1.11版本之前都绕不开GOPATH这个环境变量。GOPATH主要用来放置项目依赖包的源代码,GOPATH不区分项目,代码中任何import的路径均从GOPATH为根目录开始;但现在GOPATH已经不够用了。
不区分依赖项版本
当有多个项目时,不同项目对于依赖库的版本需求不一致时,无法在一个GOPATH下面放置不同版本的依赖项。典型的例子:当有多项目时候,A项目依赖C 1.0.0,B项目依赖C 2.0.0,由于没有依赖项版本的概念,C 1.0.0和C 2.0.0无法同时在GOPATH下共存,解决办法是分别为A项目和B项目设置GOPATH,将不同版本的C源代码放在两个GOPATH中,彼此独立(编译时切换),或者C 1.0.0和C 2.0.0两个版本更改包名。无论哪种解决方法,都需要人工判断更正,不具备便利性。
依赖项列表无法数据化
在Go Modules之前,没有任何语义化的数据可以知道当前项目的所有依赖项,需要手动找出所有依赖。对项目而言,需要将所有的依赖项全部放入源代码控制中。如果剔除某个依赖,需要在源代码中手工确认某个依赖是否剔除。
为了解决GOPATH的缺陷,Go官方和社区推出许多解决方案,比如godep、govendor、glide等,这些工具要么未彻底解决GOPATH存在的问题要么使用起来繁冗,这才催生了Go Modules的出现。
如何使用Go Modules
简单来说,Go Modules是语义化版本管理的依赖项的包管理工具;它解决了GOPATH存在的缺陷,最重要的是,它是Go官方出品。(以下内容将会涉及到标准依赖项管理和构建)
创建Modules
首先创建一个可以供其他项目使用的项目:testmod
,内容如下:
package testmod
import (
"fmt"
)
func Hi(name string) string {
return fmt.Sprintf("Hi, %s", name)
}
使用如下方式创建modules: go mod init github.com/robteix/testmod
该命令会在目录下生成go.mod文件,内容如下:
module github.com/robteix/testmod
go 1.13
在将代码推送至Github之后,其他人可以使用如下命令下载到testmod
:
go get github.com/robteix/testmod
默认情况下,go get
将会下载到testmod
master分支(在没有tags的情况下),即代码主分支。上面我们说过,Go Modules具有语义化版本管理功能的,所以可以使用go get
下载特定版本的包:
go get github.com/robteix/[email protected]
模块版本
Go Modules是版本化的,并且某些版本具有某些特殊性。Go默认语义化版本控制,语义化版本详见:[https://semver.org/lang/zh-CN/]()。
Go在查找包版本时使用仓库tags,在一些情况下,某些版本和其他版本不同:版本2和更高版本的导入路径应该与版本0和版本1不。默认情况下,Go获取仓库中可用的最新标记版本。
发布第一个版本
我们可以通过使用版本标签来发布1.0.0版本:
git tag v1.0.0
git push --tags
此时将会在Github的仓库上创建名为v1.0.0的标签。推荐的做法是创建新的代码分支,这样可以直接在分支上修改v1.0.0的问题,而不影响主分支的开发进度。
git checkout -b v1
git push -u origin v1
使用已发布的版本
现在创建一个使用testmod
包的项目:
package main
import (
"fmt"
"github.com/robteix/testmod"
)
func main() {
fmt.Printf(testmod.Hi("roberto"))
}
执行go mod init mymod
,将会生成go.mod
文件,然后go build
,此时会输出:
$ go build
go: finding github.com/robteix/testmod v1.0.0
go: downloading github.com/robteix/testmod v1.0.0
go get
命令自动执行尝试在Github上下载最新版本标签的testmod
,完成之后go.mod
文件中新增了:
require (
require github.com/robteix/testmod v1.0.0
)
意即当前项目依赖testmod
v1.0.0版本。
发布修复版本
假设testmod
v1.0.0需要进行问题修复:
// Hi returns a friendly greeting
func Hi(name string) string {
- return fmt.Sprintf("Hi, %s", name)
+ return fmt.Sprintf("Hi, %s!", name)
}
我们在v1分支中进行此修复:
$ git commit -m "Emphasize our friendliness" testmod.go
$ git tag v1.0.1
$ git push --tags origin v1
更新模块
默认情况下,出于构建中的可预测性和稳定性考虑,Go不会自动更新模块,需要手动更新依赖。可以使用如下方式更新依赖包:
- 使用
go get -u
,更新到修订版本或次要版本,即从v1.0.0更新到v1.0.1,如果v1.1.0可用,则更新到v1.1.0。 - 使用
go get -u=path
,更新到修订版本,即从v1.0.0更新到v1.0.1。 - 使用
go get [email protected]
更新到特定版本。
使用上述任一方式更新之后,go.mod中的依赖记录被更新如下:
require github.com/robteix/testmod v1.0.1
主要版本
根据语义化版本控制,主要版本与次要版本不同,主要版本可能会破坏向后兼容性。从Go模块角度来看,主要版本是完全不同的软件包:两个不兼容的库版本是两个不同的仓库。
package testmod
import (
"errors"
"fmt"
)
// Hi returns a friendly greeting in language lang
func Hi(name, lang string) (string, error) {
switch lang {
case "en":
return fmt.Sprintf("Hi, %s!", name), nil
case "pt":
return fmt.Sprintf("Oi, %s!", name), nil
case "es":
return fmt.Sprintf("¡Hola, %s!", name), nil
case "fr":
return fmt.Sprintf("Bonjour, %s!", name), nil
default:
return "", errors.New("unknown language")
}
}
现在testmod
的Hi
函数已经和v1.0.x不兼容了,是时候发布v2.x.x版本了。最佳的做法是:v2.x.x以及更高的版本更改导入路径。修改go.mod
文件中的模块导入路径(也可以使用echo命令修改):
module github.com/robteix/testmod/v2
然后创建v2分支并将版本打上v2.0.0的发布标签:
$ git commit testmod.go -m "Change Hi to allow multilang"
$ git checkout -b v2 # optional but recommended
$ echo "module github.com/robteix/testmod/v2" > go.mod
$ git commit go.mod -m "Bump version to v2"
$ git tag v2.0.0
$ git push --tags origin v2 # or master if we don't have a branch
更新到主要版本
现在修改mymode
项目引入testmod
v2.0.0版本,Go在编译时可以根据import路径自动下载依赖包:
package main
import (
"fmt"
"github.com/robteix/testmod/v2"
)
func main() {
g, err := testmod.Hi("Roberto", "pt")
if err != nil {
panic(err)
}
fmt.Println(g)
}
当运行go build
的时候,会自动下载testmod
v2.0.0版本。由于testmod
v1和v2的导入路径完全不一样,在mymod
项目里可以同时存在:
package main
import (
"fmt"
"github.com/robteix/testmod"
testmodML "github.com/robteix/testmod/v2"
)
func main() {
fmt.Println(testmod.Hi("Roberto"))
g, err := testmodML.Hi("Roberto", "pt")
if err != nil {
panic(err)
}
fmt.Println(g)
}
这里消除了依赖性管理的一个常见问题:依赖于同一库的不同版本。
依赖本地包
有一种可能出现的情况:项目的某个依赖包并不在Github或者其他代码托管网站上,而是在本地,此时需要修改go.mod
文件引入本地依赖包。
require (
3rd/module/testmod v0.0.0
)
replace 3rd/module/testmod => /usr/local/go/testmod
代码中以3rd/module/testmod
作为导入路径,编译时会根据replace
找到真实代码目录。
CI
有一种现实情况是:内部构建系统是无网络环境的,也就是说所有的依赖项都需要纳入内部版本控制中,Go Modules提供了此功能:
go mod vendor
该命令会将在当前项目根目录下创建vendor目录,然后将项目所有依赖项缓存此目录中,而此目录可以直接进入内部版本控制。在默认情况下go build将忽略vendor目录,如果要从vendor目录开始构建:
go build -mod vendor
这样做的好处可以不用依赖网络上游的版本,在内部自由使用稳定可控制的版本进行构建,go mod vendor应该会成为非开源项目的主要构建方式。