Go 语言中一直被人诟病的一个问题就是没有一个比较好用的依赖管理系统,GOPATH 的设计让开发者一直有不少怨言,在 Go 语言快速发展的过程当中也出现了一些比较优秀的依赖管理工具,好比 govendor、dep、glide 等,有一些差很少成了半官方的工具了,可是这些工具都仍是须要依赖于 GOPATH,为了完全解决这个“祸水”,随着 Go1.11 的发布,Golang 官方给咱们带来了依赖管理的全新特性 GoModules,这是 Golang 全新的一套依赖管理系统。下面咱们就来看下 Go Modules 是如何使用的。git
要使用 Go Modules 首先须要保证你环境中 Golang 版本大于1.11:github
$ go version go version go1.11.4 darwin/amd64
咱们说 Go Modules 主要就是为了消除 GOPATH 的,因此咱们新建的项目能够彻底不用放在 $GOPATH/src目录下面,任何地方均可以:golang
$ echo $GOPATH /Users/ych/devs/projects/go/
咱们在目录 /Users/ych/devs/projects下面建立一个用于测试的工做目录:docker
$ pwd /Users/ych/devs/projects $ mkdir stardust && cd stardust
在 stardust 目录下面建立一个用于字符串操做的 stringsx 的包:json
$ mkdir stringsx && cd stringsx
在包 stringsx 下面建立一个 string.go 的文件:bash
package stringsx import ( "fmt" ) func Hello(name string) string{ return fmt.Sprintf("Hello, %s", name), nil }
如今咱们的包里面的代码准备完成了,但还不是一个模块,咱们须要使用 Go Modules 来作一些工做:微信
$ export GO111MODULE=on # 开启GoModule特性
而后在项目根目录下面初始化 Go Module:markdown
$ pwd /Users/ych/devs/projects/stardust $ go mod init github.com/cnych/stardust go: creating new go.mod: module github.com/cnych/stardust
咱们这里使用了一个命令:go mod init模块名,该命令会在当前目录下面生成一个 go.mod的文件,生成的内容就是包含一个模块名称的声明:并发
module github.com/cnych/stardust
要注意 模块名很是重要,就这至关于声明了咱们的模块名称,之后要想使用该模块就须要使用这个名称来获取模块。ide
这一步操做很是简单,可是将咱们当前的包变成了一个 Module 了,如今咱们能够将这个代码推送到代码仓库上,我这里使用的是 Github,仓库地址:https://github.com/cnych/stardust
$ git init $ git add . $ git commit -am "add stringsx package content" $ git remote add origin git@github.com:cnych/stardust.git $ git push -u origin master
到这里咱们就完成了一个最简单的 Go Module 的编写,其余任何开发者想要使用咱们这个模块的用户均可以经过 goget命令来获取了:
$ go get github.com/cnych/stardust
不过上面的命令是获取 master 分支的最新代码,这样固然没有问题,但不是一个最佳实践的方式,由于颇有可能咱们这个模块会有新的内容更新,或者有一些 BUG 须要修复,若是咱们都放在 master 分支上面的话,势必会形成使用者的混乱,由于颇有可能使用者的代码在咱们模块更新后就不兼容了,不过也不用担忧,Go Moduels 就能够很好的来解决这个版本的问题。
Go Modules 是须要进行版本化管理的,就相似于咱们平时写代码同样用不一样的版原本进行区分。对于 Go Modules 的版本强烈推荐使用语义化的版本控制,对于语义化的版本控制咱们查看响应的文档说明:https://semver.org,最主要的一个版本规则以下:
版本格式:主版本号.次版本号.修订号,版本号递增规则以下:
主版本号:当你作了不兼容的 API 修改,
次版本号:当你作了向下兼容的功能性新增,
先行版本号及版本编译元数据能够加到“主版本号.次版本号.修订号”的后面,做为延伸。
咱们在使用 Go Modules 查找版本的时候会使用仓库中的 tags,而且某些版本和其余版本有一些不一样之处,好比 v2 或者更高的版本要和 v1 的版本模块的导入路径要不一样,这样才能够经过模块来区分使用的是不一样的版本,固然默认状况下,Golang 会获取仓库中最新的 tag 版本。
因此最重要的一点就是要发布咱们的模块,咱们须要使用 git tag 来标记咱们的仓库版本。
假如如今咱们的包已经准备好了,能够发布 release 包了,首先咱们就须要给当前的包打上一个 git tag,要记住使用语义化的版本,好比咱们这里第一个版本就叫:v1.0.0:
$ git tag v1.0.0 $ git push --tags
这样咱们就会在 Github 上面建立了一个名为 v1.0.0 的 tag,可是一个更好的方式是去建立一个名为 v1的新分支,这样能够方便之后修复当前版本代码中的 BUG,也不会影响到 master 或者其余分支的一些代码:
$ git checkout -b v1 $ git push -u origin v1
如今咱们就彻底不用担忧会影响到咱们以前的版本了。
如今咱们的模块已经准备好了,如今咱们来建立一个简单的程序来使用上面的模块:
$ pwd /Users/ych/devs/projects $ mkdir ch26-gomodules && cd ch26-gomodules
而后在 ch26-gomodules 目录下面建立一个 main.go 的文件:
package main import ( "fmt" "github.com/cnych/stardust/stringsx" ) func main() { fmt.Println(stringsx.Hello("cnych")) }
程序里面使用了 github.com/cnych/stardust/stringsx这个包,在之前的话咱们直接使用 goget命令将这个包拉到 GOPATH 或者 vendor 目录下面便可,如今咱们是将这个包当成 modules 来使用,首先一样的在当前目录下面初始化模块:
$ go mod init ch26-gomodules
一样的,该命令会在目录下面新建一个 go.mod的文件:
module ch26-gomodules
这个时候咱们来直接运行下咱们当前的程序:
$ go run main.go go: finding github.com/cnych/stardust v1.0.0 go: downloading github.com/cnych/stardust v1.0.0 Hello, cnych
上面的命令会去自动下载程序中导入的包,下载完成后能够查看下 go.mod文件内容:
module ch26-gomodules require github.com/cnych/stardust v1.0.0
而且还在当前目录下面生成了一个名为 go.sum的新文件,里面包含了依赖包的一些 hash 信息,能够用来确保文件和版本的正确性:
github.com/cnych/stardust v1.0.0 h1:8EcmmpIoIxq2VrzXdkwUYTD4OcMnYlZuLgNntZ+DxUE= github.com/cnych/stardust v1.0.0/go.mod h1:Qgo0xT9MhtGo0zz48gnmbT9XjO/9kuuWKIOIKVqAv28=
模块会被下载到 $GOPATH/pkg/mod目录下面去:
$ ls $GOPATH/pkg/mod/github.com/cnych stardust@v1.0.0
好比如今咱们发现以前模块中的 Hello 函数有 bug,因此咱们须要修复并发布一个新的版本:
func Hello(name string) string{ return fmt.Sprintf("Hello, %s!!!", name), nil }
这里咱们添加3个 !来模拟 bugfix。
固然要注意咱们是在 v1这个分支上来进行 fix,fix 完成后再 merge 到 master 分支上面去,而后发布一个新的版本,听从语义化的版本规则,咱们这里是修正一个 bug,因此只须要添加修正版本号便可,好比命名为 v1.0.1 版本:
$ git add . $ git commit -m "fix Hello function #123" $ git tag v1.0.1 $ git push --tags origin v1
这样咱们就发布了修正版本的模块了。
默认状况下,Golang 不会去自动更新模块的,若是自动更新的话是否是又会形成依赖管理的混乱了,因此咱们须要明确告诉 Golang 咱们须要更新模块,咱们能够经过下面几种方式来进行模块的更新:
运行 goget-u xxx命令来获取最新的模块(好比咱们这里执行的话就会从v1.0.0更新到v1.0.1版本)
运行 gogetpackage@version命令来更新到指定版本的模块
当咱们这里用任何一种方式都是能够的,好比用第二种方式:
$ go get github.com/cnych/stardust@v1.0.1
更新完成后,能够查看 go.mod文件中的依赖模块的版本变化:
module ch26-gomodules require github.com/cnych/stardust v1.0.1
这个时候一样能够查看下 mod 文件夹下面的模块:
$ ls $GOPATH/pkg/mod/github.com/cnych stardust@v1.0.0 stardust@v1.0.1
看到这里咱们是否是就明白 Go Modules 是经过怎样的方式来进行版本控制的了?每一个版本都是独立的文件夹,这样是否是就不会出现版本冲突了。
根据语义化版本规则,主版本升级是不向后兼容的,从 Go Modules 的角度来看,主版本是一个彻底不一样的模块了,由于咱们认为两个大版本之间是互相不兼容的,那么咱们怎么设计 Go Modules 来支持两个大版本呢?好比如今咱们来修改下模块中的 Hello 函数,增长对语言的一个支持:
func Hello(name, lang string) (string, error) { switch lang { case "en": return fmt.Sprintf("Hi, %s!", name), nil case "zh": return fmt.Sprintf("你好, %s!", name), nil case "fr": return fmt.Sprintf("Bonjour, %s!", name), nil default: return "", fmt.Errorf("unknow language") } }
记住要切换到 master 分支进行修改,由于 v1 分支和咱们如今修改的内容是彻底不一样的版本了。
这里咱们的函数须要两个参数,返回也是两个参数,直接按照以前 v1 版本中的函数使用确定是会出错的,因此咱们这里新版本的模块就不打算再去兼容以前 1.x 的模块了,这个时候咱们须要更新版本到 v2.0.0,那么怎么去区分这两个大版本呢?这个时候咱们去更改 v2 版本的模块路径就能够了,好比变成 github.com/cnych/stardust/v2,这样 v2 版本的模块和以前 v1 版本的模块就是两个彻底不一样的模块了,咱们在使用新版本的模块的时候只须要在模块名称后面添加上 v2 就能够了。
module github.com/cnych/stardust/v2
接下来和前面的操做同样的,给当前版本添加一个名为 v2.0.0 的 git tag,固然最好仍是建立一个名为 v2 的分支,这样能够将版本之间的影响降到最低:
$ git add . $ git commit -m "change Hello function to support lang" $ git checkout -b v2 $ git tag v2.0.0 $ git push origin v2 --tags
这样咱们 v2 版本的模块就发布成功了,并且以前咱们的程序也不会有任何的影响,由于他仍是能够继续使用现有的 v1.0.1 版本,并且使用 goget-u命令也不会拉取最新的 v2.0.0 版本代码,可是若是对于使用的用户来讲,如今要想使用 v2.0.0 版本的模块怎么办呢?
其实很简单,只须要单独引入 v2 版本的模块便可:
package main import ( "fmt" "github.com/cnych/stardust/stringsx" stringsV2 "github.com/cnych/stardust/v2/stringsx" ) func main() { fmt.Println(stringsx.Hello("cnych")) if greet, err := stringsV2.Hello("cnych", "zh"); err != nil { fmt.Println(err) } else { fmt.Println(greet) } }
这个时候咱们去执行 go run main.go命令固然一样的会去自动拉取 github.com/cnych/stardust/v2这个模块的代码了:
$ go run main.go go: finding github.com/cnych/stardust/v2 v2.0.0 go: downloading github.com/cnych/stardust/v2 v1.0.0 Hi, cnych!!! 你好, cnych!
这样咱们在同一个 go 文件中就使用了两个不兼容版本的模块。一样这个时候再次查看下 go.mod文件的变化:
module ch26-gomodules require github.com/cnych/stardust v1.0.1 require github.com/cnych/stardust/v2 v2.0.0
默认状况下,Golang 是不会从 go.mod文件中删除依赖项的,若是咱们有不使用的一些依赖项须要清理,可使用 tidy 命令:
$ go mod tidy
该命令会清除没有使用的模块,也会更新模块到指定的最新版本。
Go Modules 默认会忽略 vendor/这个目录,可是若是咱们还想将依赖放入 vendor 目录的话,咱们能够执行下面的命令:
$ go mod vendor
该命令会在项目根目录下面建立一个 vendor/的文件夹,里面会包含全部的依赖模块代码,而且会在该目录下面添加一个名为 modules.txt的文件,用来记录依赖包的一些信息,比较相似于 govendor 中的 vendor.json 文件。
不过建议仍是不要使用该命令,尽可能去忘掉 vendor 的存在,若是有一些依赖包下载不下来的,咱们可使用 GOPROXY这个参数来设置模块代理,好比:
$ export GOPROXY="https://goproxy.io"
阿里云也提供了 Go Modules 代理仓库服务:http://mirrors.aliyun.com/goproxy/,使用很简单就两步:
1.使用 go1.11 以上版本并开启 go module机制: exportGO111MODULE=on
若是你想上面的配置始终生效,能够将这两条命令添加到 .bashrc中去。
除了使用公有的 Go Modules 代理仓库服务以外,不少时候咱们在公司内部须要搭建私有的代理服务,特别是在使用 CI/CD 的时候,若是有一个私有代理仓库服务,会大大的提供应用的构建效率。
咱们可使用Athens来搭建私有的代理仓库服务,搭建很是简单,直接用 docker 镜像运行一个服务便可:
export ATHENS_STORAGE=~/athens-storage mkdir -p $ATHENS_STORAGE docker run -d -v $ATHENS_STORAGE:/var/lib/athens \ -e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens \ -e ATHENS_STORAGE_TYPE=disk \ --name goproxy \ --restart always \ -p 3000:3000 \ gomods/athens:latest
其中 ATHENS_STORAGE 是用来存放咱们下载下来的模块的本地路径,另外 ATHENS 还支持其余类型的存储,好比 内存, AWS S3 或 Minio,都是 OK 的。
而后修改 GOPROXY 配置:
export GOPROXY=http://127.0.0.1:3000
一句话:Go Modules 真的用起来很是爽,特别是消除了 GOPATH,这个东西对于 Golang 初学者来讲是很是烦人的,很难理解为何须要进入到特定目录下面才能够编写 Go 代码,如今不用担忧了,直接使用 GoModules就行。
全新Golang实战课程上线,课程地址:https://youdianzhishi.com/m/course/1011(点击查看原文了解详细信息)
扫描下面的二维码(或微信搜索 k8s技术圈)关注咱们的微信公众账号,在微信公众账号中回复 加群 便可加入到咱们的 kubernetes 讨论群里面共同窗习。