Go Modules 使用指南

3,229 阅读7分钟

go modules 是 golang 1.11 新加的特性,go命令直接支持使用modules,包括记录和解析对其他模块的依赖性。modules替换旧的基于GOPATH的方法来指定在给定构建中使用哪些源文件。

Go Module使用先决条件

  • Golang版本 >= 1.11

  • 设置GO111MODULE

GO111MODULE设置

GO111MODULE 有三个值:off, onauto(默认值)

  • GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。

  • GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。

  • GO111MODULE=auto,默认值,在$GOPATH/src 之外的目录中调用 go 命令,且当前目录或其任何父目录中使用有效的 go.mod 文件,go命令会启动modules。

当modules 功能启用时,依赖包的存放位置变更为$GOPATH/pkg,允许同一个package多个版本并存,且多个项目可以共享缓存的 module。

Go Mod 包管理工具

golang 提供了 go mod命令来管理包。

命令说明
download下载依赖包
edit编辑go.mod
graph打印模块依赖图
init在当前目录初始化mod
tidy拉取缺少的模块,移除不用的模块
vendor将依赖复制到vendor下
verify验证依赖是否正确
why解释为什么需要依赖

如何在项目中使用

  • 初始化

在项目根目录执行以下命令

$ mkdir hello-go
$ cd hello
$ go mod init hello-go

会在hello目录下,生成go.mod的文件

module example.com/hello

go 1.14
  • 更新go.mod

创建示例代码server.go,可以使用两种方式更新go.mod中的依赖

package main

import (
	"net/http"
	
	"github.com/labstack/echo"
)

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

**方式一:**在项目根目录执行

$ go run server.go

或者

$ go run ./...

./... 模式匹配当前模块中的所有包

**方法二:**执行go get 命令更新依赖

$ go get -u github.com/labstack/echo

go.mod 文件说明

示例go.mod

module example.com/hello

require (
        github.com/labstack/echo v3.3.10+incompatible // indirect
        github.com/labstack/gommon v0.2.8 // indirect
        github.com/mattn/go-colorable v0.1.1 // indirect
        github.com/mattn/go-isatty v0.0.7 // indirect
        github.com/valyala/fasttemplate v1.0.0 // indirect
        golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a // indirect
)
go 1.14

go.mod 文件中的命令

go.mod 提供了module, requirereplaceexclude 四个命令:

  • module 语句指定包的名字(路径)

  • require 语句指定的依赖项模块

  • replace 语句可以替换依赖项模块

  • exclude 语句可以忽略依赖项模块

go semver(语义化版本)

golang 官方推荐的最佳实践叫做 semver,这是一个简称,写全了就是Semantic Versioning,也就是语义化版本,如v0.1.1 ,版本格式:主版本号.次版本号.修订号,版本号递增规则如下:

主版本号:当你做了不兼容的 API 修改,

次版本号:当你做了向下兼容的功能性新增,

修订号:当你做了向下兼容的问题修正。

  • 语义化版本的好处

semver 简化版本指定的作用是显而易见的,然而仅此一条理由显然有点缺乏说服力,通过semver对版本进行严格的约束,可以最大程度地保证向后兼容以及避免 “breaking changes”,而这些都是 golang 所追求的。两者一拍即合,所以 go modules 提供了语义化版本的支持。

如果你使用和发布的包没有版本tag或者处于v1.x版本,那么你可能体会不到什么区别,主要的区别体现在v2.0.0以及更高版本的包上。按照语义化版本的约定,当出现v2.0.0的时候一定表示发生了重大变化,很可能无法保证向后兼容,这时候应该如何处理呢?答案很简单,我们为包的导入路径的末尾附加版本信息即可,例如:

module my-module/v2

require (
  some/pkg/v2 v2.0.0
  some/pkg/v2/mod1 v2.0.0
  my/pkg/v3 v3.0.1
)

格式总结为pkgpath/vN,其中N是大于1的主要版本号。在代码里导入时也需要附带上这个版本信息,如import "some/pkg/v2"。如此一来包的导入路径发生了变化,也不用担心名称相同的对象需要向后兼容的限制了,因为golang认为不同的导入路径意味着不同的包。

不过这里有几个例外可以不用参照这种写法:

  1. 当使用gopkg.in格式时可以使用等价的require gopkg.in/some/pkg.v2 v2.0.0

  2. 在版本信息后加上+incompatible就可以不需要指定/vN,例如:require some/pkg v2.0.0+incompatible

  3. 使用go1.11时设置GO111MODULE=off将取消这种限制,当然go1.12里就不能这么干了

除此以外的情况如果直接使用v2+版本将会导致go mod报错。

v2+版本的包允许和其他不同大版本的包同时存在(前提是添加了/vN),它们将被当做不同的包来处理。另外/vN并不会影响你的仓库,不需要创建一个v2对应的仓库,这只是go modules添加的一种附加信息而已。当然如果你不想遵循这一规范或者需要兼容现有代码,那么指定+incompatible会是一个合理的选择。不过如其字面意思,go modules不推荐这种行为。

最后总结一下go包升级过程:

当你发布一个v2+版本的库时,需要进行以下操作:

  • module my-module改成module my-module/v2

  • 将源代码中使用了v2+版本包的import语句从import "my-module"改为import "my-module/v2"

  • 仔细检查你的代码中所有my-module包的版本是否统一,修改那些不兼容的问题

  • 在changelog中仔细列出所有breaking changes

**注意:**如果你觉得前面四步过于繁琐,注明你的用户需要指定+incompatible是一个暂时性的解决方案。

go.mod中的两种依赖版本格式

  • 语义化(semver)版本git tag
github.com/labstack/gommon v0.2.8
  • 伪版本

如果一个 module 没有有效的 semver 版本,那么 go.mod 将通过一个叫做 “伪版本“ 的东西来记录版本。

”伪版本“ 的通常形式是 vX.0.0-yyyymmddhhmmss-abcdefabcdef。 比如:

golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a

其中: v0.0.0 表示 semver 版本号, 20170915032832 表示这个版本的时间。 14c0d48ead0c 表示这次提交的 git hash。

如何解决依赖下载失败的问题

由于某些已知的原因,并不是所有的package都能成功下载,比如:golang.org下的包

在go.mod文件中使用replace命令

modules 可以通过在 go.mod 文件中使用 replace 指令替换成github上对应的库,比如:

replace (
	golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a => github.com/golang/crypto v0.0.0-20190313024323-a1f597ede03a
)

设置GOPROXY

Go在1.13加入了Go Proxy的支持

export GOPROXY=https://goproxy.io

或者

export GOPROXY=https://goproxy.cn

  • GOPROXY说明

GOPROXY指镜像递增和获取顺序,每个镜像URL之间用,隔开,按照优先顺序排序,**direct**表示源站。

默认值:

GOPROXY="https://proxy.golang.org,direct"

这是官方的说法:

The default setting for GOPROXY is

"proxy.golang.org,direct", which means to try the

Go module mirror run by Google and fall back to a direct connection

if the proxy reports that it does not have the module (HTTP error 404 or 410).

注意 :GOPROXY=off 表示“禁止从互联网下载任何依赖”,GOPROXY=direct 表示“依赖一律从源站下载”。

对go.sum文件的理解

也许你知道npm的package-lock.json的作用,它会记录所有库的准确版本,来源以及校验和,从而帮助开发者使用正确版本的包。通常我们发布时不会带上它,因为package.json已经够用,而package-lock.json的内容过于详细反而会对版本控制以及变更记录等带来负面影响。

如果看到go.sum文件的话,也许你会觉得它和package-lock.json一样也是一个锁文件,那就大错特错了。go.sum不是锁文件。

更准确地来说,go.sum是一个构建状态跟踪文件。它会记录当前module所有的顶层和间接依赖,以及这些依赖的校验和,从而提供一个可以100%复现的构建过程并对构建对象提供安全性的保证。

go.sum同时还会保留过去使用的包的版本信息,以便日后可能的版本回退,这一点也与普通的锁文件不同。所以go.sum并不是包管理器的锁文件。

因此我们应该把go.sum和go.mod一同添加进版本控制工具的跟踪列表,同时需要随着你的模块一起发布。如果你发布的模块中不包含此文件,使用者在构建时会报错,同时还可能出现安全风险(go.sum提供了安全性的校验)。

如何在go modules中使用vendor目录

golang一直提供了工具选择上的自由性,如果你不喜欢go mod的缓存方式,你可以使用go mod vendor回到godep或govendor使用的vendor目录进行包管理的方式。

当然这个命令并不能让你从godep之类的工具迁移到go modules,它只是单纯地把go.sum中的所有依赖下载到vendor目录里,如果你用它迁移godep你会发现vendor目录里的包回合godep指定的产生相当大的差异,所以请务必不要这样做。

  • 如何进行编译

使用go build -mod=vendor来构建项目,因为在go modules模式下go build是屏蔽vendor机制的,所以需要特定参数-mod=vendor重新开启vendor机制。

一些常用的命令

  • go get -u github.com/some/pkg 更新次版本号,由于主版本号的不兼容,所以不会更新主版本号。

  • go get -u=patch 更新修订号

  • go list -m all 查看所有依赖的 module 以及版本

  • go list -u -m all 查看可用的次版本号修订号的更新

  • go mod tidy 删除 go.mod 中没用到的 module

参考