go mod是什么

Golang从诞生之初就一直有个被诟病的问题:缺少一个行之有效的“官方”包依赖管理工具。其原因是在Google内部,所有人都是在一个代码库上进行开发的,因此并不是非常需要。但Golang变成一个社区化的工程语言之后,这个问题被放大了。

  1. GOPATH不符合一般开发者习惯,大部分人更习惯maven、node modules之类的方式
  2. 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,但最终这个命令是会消失的。

快速开始:

  1. 把之前的工程(例如我以前写的netproc)拷贝到$GOPATH/src之外(重点1)
  2. 在工程目录下执行:go mod init {module name},该命令会创建一个go.mod文件(重点2)
  3. 然后在该目录下执行 go build netproc.go ,就可以了。你将看到:
1
2
3
4
$ go build netproc.go
go: finding github.com/mitchellh/go-wordwrap latest
go: finding github.com/maruel/panicparse/stack latest
go: finding github.com/nsf/termbox-go latest

go命令(‘go build’, ‘go test’, 甚至 ‘go list’)执行时,会自己去修改go.mod文件,执行后go.mod文件新内容如下:

1
2
3
4
5
6
7
8
9
10
module github.com/silenceshell/netproc

require (
github.com/gizak/termui v2.2.0+incompatible
github.com/maruel/panicparse v1.1.1 // indirect
github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20180828145344-9e67c67572bc // indirect
github.com/nsf/termbox-go v0.0.0-20180819125858-b66b20ab708e // indirect
github.com/spf13/pflag v1.0.2 // indirect
)

可见,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
2
3
4
5
6
~/Code/Go/src/github.com/silenceshell/netproc$ GO111MODULE=on go build netproc.go
~/Code/Go/src/github.com/silenceshell/netproc$
~/Code/Go/src/github.com/silenceshell/netproc$ GO111MODULE=off go build netproc.go
netproc.go:7:2: cannot find package "github.com/gizak/termui" in any of:
/usr/local/go/src/github.com/gizak/termui (from $GOROOT)
/Users/silenceshell/Code/Go/src/github.com/gizak/termui (from $GOPATH)

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, requirereplaceexclude 四个命令

  • module 语句指定包的名字(路径)
  • require 语句指定的依赖项模块
  • replace 语句可以替换依赖项模块
  • exclude 语句可以忽略依赖项模块

执行 go run 运行代码会发现 go mod 会自动查找依赖自动下载,接着我们查看go.mod 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat go.mod

module hello

go 1.12

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 module 安装 package 的原則是先拉最新的 release tag,若无tag则拉最新的commit

go 会自动生成一个 go.sum 文件来记录 dependency tree:

1
2
3
4
5
6
7
8
$ cat go.sum
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
... 省略很多行

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
2
3
4
5
6
7
package utils

import “fmt”

func PrintText(text string) {
fmt.Println(text)
}

在根目录下的hello.go文件就可以 import “hello/utils” 引用utils

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"hello/utils"
"github.com/astaxie/beego"
)

func main() {
utils.PrintText("Hi")
beego.Run()
}

问题五:以前老项目如何用新的包管理

  1. 如果用auto模式,把项目移动到$GOPATH/src
  2. 进入目录,运行 go mod init + 模块名称
  3. go build 或者 go run 一次