仓库源文站点原文


layout: post title: "Go Modules详解" categories: Go tags: modules

author: 张乘辉

创建 module

在默认情况下,$GOPATH 默认情况下是不支持 go mudules 的,我们需要在项目目录下手动执行以下命令:

$ export GO111MODULE=on

这也表明了 go 要利用 modules 机制消灭 $GOPATH 的决心啊!

为了配合 go modules 机制,我们 $GOPATH 以外的目录创建一个 testmod 的包:

$ mkdir testmod
$ cd testmod

$ echo 'package testmod
        import "fmt"
    func SayHello(name string) string {
   return fmt.Sprintf("Hello, %s", name)
}' >> testmod.go
$ go mod init github.com/objcoding/testmod
go: creating new go.mod: module github.com/objcoding/testmod

以上命令会在项目中创建一个 go.mod 文件,初始化内容如下:

module github.com/objcoding/testmod

这时,我们的项目已经成为了一个 module 了。

$ git init
$ git add *
$ git commit -am "First commit"
$ git push -u origin master

在这里我也着重说下关于项目依赖包引用地址的问题,这个问题虽小,但也确实很困扰人,所以必须得说一下:

go mudules 出现之前,在一个项目中有很多个包,在项目内,有些包需要依赖项目内其它包,假设项目有个包,相对于 gopath 的地址是 objcoding/mypackage,在项目内其它包引用这个包时,就可以通过以下引用:

import myproject/mypackage

但你有没有想过,当别的项目需要引用你的项目中的某些包,那么就需要远程下载依赖包了,这时就需要项目的仓库地址引用,比如下面这样:

import github.com/objcoding/myproject/mypackage

go modules 发布之后,就完全统一了包引用的地址,如上面我们说的创建 go.mod 文件后,初始化内容的第一行就是我们说的项目依赖路径,通常来说该地址就是项目的仓库地址,所有需要引用项目包的地址都填写这个地址,无论是内部之间引用还是外部引用,举个例子,goim 的内部包引用:

go.mod module:

内部包引用:

也即是说,在项目 启用了 go modules 之后,引用包必须跟 go mod 文件第一行包名一样,

依赖的包都会保存在 ${GOPATH}/pkg/mod 文件夹中了,我们也可以在项目底部那里查看依赖包:

但也有可能会出现依赖包地址正确但会报红的情况,这时极有可能是你在 Goland 编辑器中没有将项目设置为 go modules 项目,具体设置如下:

勾选了该选项之后,就会在 External Libraries 中出现 Go Modules 目录。

版本规则

go modules 是一个版本化依赖管理系统,版本需要遵循一些规则,比如版本号需要遵循以下格式:

vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef
vX.0.0-yyyymmddhhmmss-abcdefabcdef
vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdefabcdef
vX.Y.Z

vX.Y.Z 是我们仓库打的标签版本,也就是 go modules 是根据仓库标签来确定版本号的,因此我们发布版本时,需要给我们的仓库打上一个标签版本号。

也就是版本号 + 时间戳 +hash,我们自己指定版本时只需要制定版本号即可,没有版本 tag 的则需要找到对应 commit 的时间和 hash 值。

还有一个重要的规则是,版本 0 和 1,最好需要有不同的依赖路径,如:v1.0.0 和 v2.0.0 是有不同的依赖路径,下面会详细介绍一下这个版本规则。

发布版本

了解了 go modules 的版本规则后,现在我们发布一下该项目的版本:

$ git tag v1.0.0
$ git push --tags

这时我们最好还需要创建一条 v1 分支,以便我们在其它分支写代码不会影响到 v1.0.0 版本:

$ git checkout -b v1
$ git push -u origin v1

使用 module

现在我们把刚刚做好的 module,拿过来用,新建一个 gomodules 项目:

package main

import (
    "fmt"
    "github.com/objcoding/testmod"
)

func main() {
    fmt.Println(testmod.SayHello("张乘辉"))
}

以前我们可以直接通过 go get 命令获取依赖包,但是对于一个 module 项目来说,就远远比这个有趣多了,现将项目初始化成 module 项目:

$ go mod init

这时 go build 等命令就会下载依赖包,并把依赖信息添加到 go.mod 文件中,同时把依赖版本哈希信息存到 go.sum 文件中:

$ go build

go: finding github.com/objcoding/testmod v1.0.0
go: downloading github.com/objcoding/testmod v1.0.0

这时,go.mod 文件内容如下:

module gomodules
require github.com/objcoding/testmod v1.0.0

go.sum 文件内容如下:

github.com/objcoding/testmod v1.0.0 h1:fGa15gBXoqkG0BVkQGP9i5Pg2nt8nayFpHFf+GLiX6A=
github.com/objcoding/testmod v1.0.0/go.mod h1:LGpYEmOLZhLQC3JW88STU2Ja3rsfoGZmsidsHJhDNGU=

这里还需要注意的是,有时候我们引用 golang.org/x/ 的一些包,但发现在伟大的天朝这个地址是被qian了,但是我们程序员也充分发挥了勤奋好学的优良传统,在 go modules 中设置了 goproxy 机制,如果 go modules 设置了代理,会优先从代理中下载依赖包,在 /etc/profile 中加入以下内容:

export GOPROXY="https://goproxy.io"

goproxy.io 谷歌官方的代理地址,当然还有很多国内优秀的第三方代理。

你也可以在 Goland编辑器中设置:

升级版本

现在我们来升级一下 testmod 项目:

$ cd gomodules
$ echo 'package testmod
        import "fmt"
    func SayHello(name string) string {
   return fmt.Sprintf("你好, %s", name)
}' >> testmod.go

我把「Hello」改成「你好」,我们把这个修改在 v1 分支中进行:

$ git commit -m "update testmod" testmod.go
$ git tag v1.0.1
$ git push --tags origin v1

现在我们的 项目已经升级到 v1.0.1 版本了,我们可以有多种方式获取这个版本依赖,go1.11 中,go get 拥有了很多新特性,我们可以直接通过以下命令获取 v1.01 版本依赖:

$ go get github.com/objcoding/testmod@v1.0.1

也可以通过 go mod 命令:

$ go mod edit -require="github.com/objcoding/testmod@v1.0.1"

$ go mod tidy

go mod edit -require 可以主动修改 go.md 文件中依赖的版本号,然后通过 go mod tidy 对版本进行更新,这是一条神一样的命令,它会自动清理掉不需要的依赖项,同时可以将依赖项更新到当前版本。

主要版本升级

上面版本规则说了,版本 0 和 1,即大版本更新,最好需要有不同的依赖路径,如:v1.0.0 和 v2.0.0 是有不同的依赖路径,那么用 go modules 怎么实现呢,我们可以通过修改 go.mod 文件第一行添加新路径:

$ cd testmod
$ echo 'module github.com/objcoding/testmod/v2' >> go.mod

然后我们修改 testmod 函数 Hi():

$ cd testmod

$ echo 'package testmod
import (
    "fmt"
)
func SayHello(name, str string) string {
    return fmt.Sprintf("你好, %s, %s", name, str)
}' >> testmod.go

这时,SayHello() 方法将不兼容 v1 版本,我们需要新建一个 v2.0.0 版本,还是老样子,我们最好在 v2.0.0 版本新建一条 v2 分分支,将 v2.0.0 版本的代码写到这条分支中(这只是一个规范,实际上你将代码也写到任何分支中都行,Go并没有这个规范):

$ git add *
$ git checkout -b v2
$ git commit testmod.go -m "v2.0.0"
$ git tag v2.0.0
$ git push --tags origin v2

这时候 testmod 的版本已经更新到 v2.0.0 版本了,该版本并不兼容以前的版本,但是目前项目依然只能获取到 v1.0.1 的依赖版本,因为我们 testmod 的 module 路径已经加上 v2 了,因此并不会出现冲突的问题,那么如果我们需要使用 v2.0.0 版本呢?我们只需要在项目中更改一下 import 路径:

package main

import (
    "fmt"
    "github.com/objcoding/testmod/v2"
)
func main() {
    fmt.Println(testmod.SayHello("张乘辉", "最近过得怎样"))
}

执行:

go mod tidy

这时我们把 testmod 依赖版本号更新到了 v2.0.0 版本了,虽然是此时的 import 路径是以 “v2” 结尾,但是 Go 很人性化,我们依然可以使用 testmod 来使用。