go mod是什么
Golang从诞生之初就一直有个被诟病的问题:缺少一个行之有效的“官方”包依赖管理工具。其原因是在Google内部,所有人都是在一个代码库上进行开发的,因此并不是非常需要。但Golang变成一个社区化的工程语言之后,这个问题被放大了。
- GOPATH不符合一般开发者习惯,大部分人更习惯maven、node modules之类的方式
- GOPATH无法有效的管理版本依赖,没有一个地方能够表明依赖包的具体版本号,无法形成有效的版本配套关系
在Golang 1.5发布了vendor特性之后,社区在vendor基础上开发了很多包管理工具,例如glide, dep(这个最悲催,已经半官方了,结果横刀杀出来一个go mod),等等,具体参见拙文go依赖包管理工具对比 。但说实话,都不是非常满意。
你可能会说dep,glide我用的都挺爽的,有什么不满意的?
其实我不喜欢的是vendor这个特性。开发过前端的同学对node modules一定印象深刻,得益于前端混乱的包管理,一个普通的web前端,其node modules往往非常巨大,而且它是每工程的,也就是说如果有2个前端工程,就会有两份巨大的node moudles,里面有成千上万个文件,常常造成IDE挂死,也非常浪费硬盘。
那么,vendor是不是到后期也会变成这样呢?同样的库,同样的版本,就因为在不同的工程里用了,就要在vendor里单独搞一份,不浪费吗?所以这些基于vendor的包管理工具,都会有这个问题。
相比之下maven这种本地缓存库的管理方式就好很多。
Golang 1.11 版本引入的 go mod ,其思想类似maven:摒弃vendor和GOPATH,拥抱本地库。
先来看看怎么用。
如何使用go mod
在1.11版本引入,尚且是初期版本。考虑向前兼容,目前仍然可以用go get,但最终这个命令是会消失的。
快速开始:
- 把之前的工程(例如我以前写的netproc)拷贝到
$GOPATH/src
之外(重点1) - 在工程目录下执行:go mod init {module name},该命令会创建一个
go.mod
文件(重点2) - 然后在该目录下执行
go build netproc.go
,就可以了。你将看到:
1 | $ go build netproc.go |
go命令(‘go build’, ‘go test’, 甚至 ‘go list’)执行时,会自己去修改go.mod文件,执行后go.mod文件新内容如下:
1 | module github.com/silenceshell/netproc |
可见,go.mod中记录了依赖包及其版本号。
更好的控制
如果想更好的控制,可以修改 GO111MODULE
临时环境变量。
GO111MODULE
的取值为 off, on, or auto (默认值,因此前面例子里需要注意2个重点)。
- off: GOPATH mode,查找vendor和GOPATH目录
- on:module-aware mode,使用 go module,忽略GOPATH目录
- auto:如果当前目录不在$GOPATH 并且 当前目录(或者父目录)下有go.mod文件,则使用
GO111MODULE
, 否则仍旧使用 GOPATH mode。
试试:
1 | ~/Code/Go/src/github.com/silenceshell/netproc$ GO111MODULE=on go build netproc.go |
on的时候因为我前面已经build过一次,所以直接成功了;但改为off时,会因为找不到包而报错。
注意,go modules 下载的包在 GOPATH/pkg/mod
,这就是前面所说的 ‘maven way’;安装的命令仍在 GOPATH/bin
。
一个简单介绍,更详细的后面使用了再补充。
go mod
golang 提供了 go mod
命令来管理包。
go mod 有以下命令:
命令 | 说明 |
---|---|
download | download modules to local cache(下载依赖包) |
edit | edit go.mod from tools or scripts(编辑go.mod |
graph | print module requirement graph (打印模块依赖图) |
init | initialize new module in current directory(在当前目录初始化mod) |
tidy | add missing and remove unused modules(拉取缺少的模块,移除不用的模块) |
vendor | make vendored copy of dependencies(将依赖复制到vendor下) |
verify | verify dependencies have expected content (验证依赖是否正确) |
why | explain why packages or modules are needed(解释为什么需要依赖) |
go.mod 提供了module
, require
、replace
和exclude
四个命令
module
语句指定包的名字(路径)require
语句指定的依赖项模块replace
语句可以替换依赖项模块exclude
语句可以忽略依赖项模块
执行 go run
运行代码会发现 go mod 会自动查找依赖自动下载,接着我们查看go.mod 内容
1 | $ cat go.mod |
go module 安装 package 的原則是先拉最新的 release tag,若无tag则拉最新的commit
go 会自动生成一个 go.sum 文件来记录 dependency tree:
1 | $ cat go.sum |
go.sum文件:
包含特定模块版本内容的预期加密哈希
go命令使用go.sum文件确保这些模块的未来下载检索与第一次下载相同的位,以确保项目所依赖的模块不会出现意外更改,无论是出于恶意、意外还是其他原因。 go.mod和go.sum都应检入版本控制。go.sum 不需要手工维护,所以可以不用太关注。
再次执行 go run
发现跳过了检查并安装依赖的步骤。
可以使用命令 go list -m -u all
来检查可以升级的package,使用go get -u need-upgrade-package
升级后会将新的依赖版本更新到go.mod * 也可以使用 go get -u
升级所有依赖
mod基本操作
- 初始化一个moudle,模块名为你项目名
1 | go mod init 模块名 |
- 下载modules到本地cache
目前所有模块版本数据均缓存在
$GOPATH/pkg/mod
和$GOPATH/pkg/sum
下
1 | go mod download |
- 编辑go.mod文件 选项有
-json
、-require
和-exclude
,可以使用帮助go help mod edit
1 | go mod edit |
- 以文本模式打印模块需求图
1 | go mod graph |
- 删除错误或者不使用的modules
1 | go mod tidy |
- 生成vendor目录
1 | go mod vendor |
- 验证依赖是否正确
1 | go mod verify |
- 查找依赖
1 | go mod why |
- go mod init # 初始化go.mod
- go mod tidy # 更新依赖文件
- go mod download # 下载依赖文件
- go mod vendor # 将依赖转移至本地的vendor文件
- go mod edit # 手动修改依赖文件
- go mod graph # 打印依赖图
- go mod verify # 校验依赖
问题一:依赖的包下载到哪里了?还在GOPATH里吗?
不在。
使用Go的包管理方式,依赖的第三方包被下载到了$GOPATH/pkg/mod
路径下。
问题二: 依赖包的版本是怎么控制的?
在上一个问题里,可以看到最终下载在$GOPATH/pkg/mod
下的包中最后会有一个版本号 v1.0.5,也就是说,$GOPATH/pkg/mod
里可以保存相同包的不同版本。
版本是在go.mod中指定的。如果,在go.mod中没有指定,go命令会自动下载代码中的依赖的最新版本,本例就是自动下载最新的版本。如果,在go.mod用require语句指定包和版本 ,go命令会根据指定的路径和版本下载包,
指定版本时可以用latest
,这样它会自动下载指定包的最新版本;
问题三: 可以把项目放在$GOPATH/src下吗?
可以。但是go会根据GO111MODULE的值而采取不同的处理方式,默认情况下,GO111MODULE=auto
自动模式
1.auto 自动模式下,项目在$GOPATH/src
里会使用$GOPATH/src
的依赖包,在$GOPATH/src外,就使用go.mod 里 require的包
2.on 开启模式,1.12后,无论在$GOPATH/src
里还是在外面,都会使用go.mod 里 require的包
3.off 关闭模式,就是老规矩。
问题三: 依赖包中的地址失效了怎么办?比如 golang.org/x/… 下的包都无法下载怎么办?
在go快速发展的过程中,有一些依赖包地址变更了。以前的做法:
1.修改源码,用新路径替换import的地址
2.git clone 或 go get 新包后,copy到$GOPATH/src里旧的路径下
无论什么方法,都不便于维护,特别是多人协同开发时。
使用go.mod就简单了,在go.mod文件里用 replace 替换包,例如
1 | replace golang.org/x/text => github.com/golang/text latest |
这样,go会用 github.com/golang/text 替代golang.org/x/text,原理就是下载github.com/golang/text 的最新版本到 $GOPATH/pkg/mod/golang.org/x/text
下。
问题四: init生成的go.mod的模块名称有什么用?
本例里,用 go mod init hello 生成的go.mod文件里的第一行会申明module hello
因为我们的项目已经不在$GOPATH/src
里了,那么引用自己怎么办?就用模块名+路径。
例如,在项目下新建目录 utils,创建一个tools.go文件:
1 | package utils |
在根目录下的hello.go文件就可以 import “hello/utils” 引用utils
1 | package main |
问题五:以前老项目如何用新的包管理
- 如果用auto模式,把项目移动到
$GOPATH/src
外 - 进入目录,运行
go mod init + 模块名称
go build
或者go run
一次