首发于Golang Inside
Go Modules 不完全教程

Go Modules 不完全教程

对 Golang 感兴趣的同学欢迎关注公众号:golang-experts

Go Modules 是 Golang 官方最近几个版本推出的原生的包管理方式,在此之前,社区也不乏多种包管理方案。在讨论 Go Modules 之前,我们先回顾一下 Golang 的包管理历史的发展。然后讨论一下 Go Modules 的使用以及一些特性,篇幅有限,有些地方不方便展开,后面有时间再深入。行文仓促,不当之处,多多指教。

0. 包管理的历史

Golang 的包管理一直被大众所诟病的一个点,但是我们可以看到现在确实是在往好的方向进行发展。下面是官方的包管理工具的发展历史:

  • 在 1.5 版本之前,所有的依赖包都是存放在 GOPATH 下,没有版本控制。这个类似 Google 使用单一仓库来管理代码的方式。这种方式的最大的弊端就是无法实现包的多版本控制,比如项目 A 和项目 B 依赖于不同版本的 package,如果 package 没有做到完全的向前兼容,往往会导致一些问题。
  • 1.5 版本推出了 vendor 机制。所谓 vendor 机制,就是每个项目的根目录下可以有一个 vendor 目录,里面存放了该项目的依赖的 package。go build 的时候会先去 vendor 目录查找依赖,如果没有找到会再去 GOPATH 目录下查找。
  • 1.9 版本推出了实验性质的包管理工具 dep,这里把 dep 归结为 Golang 官方的包管理方式可能有一些不太准确。关于 dep 的争议颇多,比如为什么官方后来没有直接使用 dep 而是弄了一个新的 modules,具体细节这里不太方便展开。
  • 1.11 版本推出 modules 机制,简称 mod,也就是本文要讨论的重点。modules 的原型其实是 vgo,关于 vgo,可以参考文章末尾的参考链接。

除此之外,社区也一直在有几个活跃的包管理工具,使用广泛且具有代表性的主要有下面几个:

  • godep
  • glide
  • govendor

关于这几种包管理工具的使用这里就不再详述了。

1. modules 简单使用方式

下面看一下 modules 的简单使用方式。

准备工作

Golang 版本:1.12.3。在 1.12 版本之前,使用 Go modules 之前需要环境变量 GO111MODULE:

  • GO111MODULE=off: 不适用 modules 功能。
  • GO111MODULE=on: 使用 modules 功能,不会去 GOPATH 下面查找依赖包。
  • GO111MODULE=auto: Golang 自己检测是不是使用 modules 功能。

在 GOPATH 之外创建一个项目 mod-demo,包含一个 main.go 文件,内容如下:

package main

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

func main() {
    beego.Run()
}

初始化

初始化很简单,在项目根目录执行命令 go mod init . ,然后会生成一个 go.mod 文件如下。

➜  mod-demo $ go mod init mod-demo
go: creating new go.mod: module .
➜  mod-demo $ ls
go.mod     main.go
➜  mod-demo $ cat go.mod
module .

go 1.12

这里比较关键的就是这个 go.mod 文件,这个文件中标识了我们的项目的依赖的 package 的版本。执行 init 暂时还没有将所有的依赖管理起来。我们需要将程序 run 起来(比如执行 go run/test),或者 build(执行命令 go build)的时候,才会触发依赖的解析。

比如使用 go run 即可触发 modules 工作。

➜  mod-demo $ go run main.go
go: extracting github.com/astaxie/beego v1.12.0
2019/09/08 23:23:03.507 [I]  http server Running on http://:8080

这个时候我们再查看 go.mod 文件:

➜  mod-demo $ cat go.mod
module mod-demo

go 1.12

require (
    github.com/astaxie/beego v1.12.0
    github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
)

同时我们发现项目目录下多了一个 go.sum 用来记录每个 package 的版本和哈希值。go.mod 文件正常情况会包含 module 和 require 模块,除此之外还可以包含 replace 和 exclude 模块。

这些 package 并不是直接存储到 \$GOPATH/src,而是存储到 \$GOPATH/pkg/mod 下面,不同版本并存的方式。

➜  mod-demo $ ls $GOPATH/pkg/mod/github.com/astaxie
beego@v1.11.0 beego@v1.11.1 beego@v1.12.0

依赖升级(降级)

可以使用如下命令来查看当前项目依赖的所有的包。

➜  mod-demo $ go list -m -u all
go: finding github.com/beego/x2j latest
go: finding github.com/cloudflare/golz4 latest
go: finding github.com/siddontang/go latest
go: finding github.com/shiena/ansicolor latest
go: finding github.com/couchbase/go-couchbase latest
go: finding github.com/siddontang/rdb latest
go: finding gopkg.in/check.v1 latest
go: finding github.com/siddontang/ledisdb latest
go: finding github.com/ssdb/gossdb latest
go: finding github.com/couchbase/gomemcached latest
go: finding github.com/wendal/errors latest
go: finding github.com/couchbase/goutils latest
go: finding golang.org/x/net latest
go: finding github.com/cupcake/rdb latest
go: finding github.com/beego/goyaml2 latest
go: finding golang.org/x/crypto latest
go: finding github.com/bradfitz/gomemcache latest
github.com/Knetic/govaluate v3.0.0+incompatible
github.com/OwnLocal/goes v1.0.0
github.com/astaxie/beego v1.12.0
...

如果我想要升级(降级)某个 package 则只需要 go get 即可,比如:

go get package@version

需要注意的是,在 modules 模式开启和关闭的情况下,go get 的使用方式不是完全相同的。在 modules 模式开启的情况下,可以通过在 package 后面添加 @version 来表明要升级(降级)到某个版本。如果没有指明 version 的情况下,则默认先下载打了 tag 的 release 版本,比如 v0.4.5 或者 v1.2.3;如果没有 release 版本,则下载最新的 pre release 版本,比如 v0.0.1-pre1。如果还没有则下载最新的 commit。这个地方给我们的一个启示是如果我们不按规范的方式来命名我们的 package 的 tag,则 modules 是无法管理的。version 的格式为 v(major).(minor).(patch) ,更多信息可以参考:semver.org/

比如我们现在想将我们依赖中的 beego 项目的版本改为 v1.11.1,则可以像如下操作。我们发现执行完 go get 之后, go.mod 中的项目的版本也相应改变了。

➜  mod-demo $ go get github.com/astaxie/beego@v1.11.1
go: finding github.com/astaxie/beego v1.11.1
go: downloading github.com/astaxie/beego v1.11.1
go: extracting github.com/astaxie/beego v1.11.1
➜  mod-demo $ cat go.mod
module .

go 1.12

require (
    github.com/OwnLocal/goes v1.0.0 // indirect
    github.com/astaxie/beego v1.11.1 // indirect
    github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect
)

在 modules 开启的模式下,go get 还支持 version 模糊查询,比如 > v1.0.0 表示大于 v1.0.0 的可使用版本;< v1.12.0 表示小于 v1.12.0 版本下最近可用的版本。version 的比较规则按照 version 的各个字段来展开。

除了指定版本,我们还可以使用如下命名使用最近的可行的版本:

  • go get -u 使用最新的 minor 或者 patch 版本
  • go get -u=patch 使用最新的 patch 版本

vendor

我们知道 Go 1.5 推出了 vendor 机制,go mod 也可以支持 vendor 机制,将依赖包拷贝到 vendor 目录。但是像一些 test case 里面的依赖包并不会拷贝的 vendor 目录中。

➜  mod-demo $ go help mod vendor
usage: go mod vendor [-v]

Vendor resets the main module's vendor directory to include all packages
needed to build and test all the main module's packages.
It does not include test code for vendored packages.

2. modules 特性

上面介绍了 go modules 的简单使用方法,但是 modules 的一些更高级的特性没有介绍,将在下面进行展开。

GoProxy

proxy 顾名思义,代理服务器。众所周知,有些 Golang 的 package 在国内是无法直接 go get 的。在之前,我们解决这个问题,一般都是通过设置 http_proxy/https_proxy 来解决。GoProxy 相当于官方提供了一种 proxy 的方式让用户来进行包下载。要使用 GoProxy 只需要设置环境变量 GOPROXY 即可。目前公开的 GOPROXY 有:

  • goproxy.io:
  • goproxy.cn: 由七牛云提供,参考 github repo

当然你也可以实现自己的 GoProxy 服务,比如项目中的依赖包含外部依赖和内部依赖的时候,那么只需要实现 module proxy protocal 协议即可。

值得注意的是,在最新 release 的 Go 1.13 版本中默认将 GOPROXY 设置为 proxy.golang.org,这个对于国内的开发者是无法直接使用的。所以如果升级了 Go 1.13 版本一定要把 GOPROXY 手动改掉。

RePlace

replace 主要为了解决某些包发生改名的问题。

对于另外一种场景有的时候也是有用的,比如对于有些 golang.org/x/ 下面的包由于某些原因在国内是下载不了的,但是对应的包在 github 上面是有一份拷贝的,这个时候我们就可以将 go.mod 中的包进行 replace 操作。

下面是一个 Beego 项目的 go.mod 的 replace 的示例。

replace golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 => github.com/golang/crypto v0.0.0-20181127143415-eb0de9b17e85

replace gopkg.in/yaml.v2 v2.2.1 => github.com/go-yaml/yaml v0.0.0-20180328195020-5420a8b6744d

SubCommand

modules 支持的 subcommand 如下。

➜  mod-demo $ go help mod
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

Usage:

    go mod <command> [arguments]

The commands are:

    download    download modules to local cache
    edit        edit go.mod from tools or scripts
    graph       print module requirement graph
    init        initialize new module in current directory
    tidy        add missing and remove unused modules
    vendor      make vendored copy of dependencies
    verify      verify dependencies have expected content
    why         explain why packages or modules are needed

Use "go help mod <command>" for more information about a command.

每个 subcommand 的含义如下:

  • download: 下载 modules 到本地缓存
  • edit: 提供一种命令行交互修改 go.mod 的方式
  • graph: 将 module 的依赖图在命令行打印出来,其实并不是很直观
  • init: 初始化 modules,会生成一个 go.mod 文件
  • tidy: 清理 go.mod 中的依赖,会添加缺失的依赖,同时移除没有用到的依赖
  • vendor: 将依赖包打包拷贝到项目的 vendor 目录下,值得注意的是并不会将 test code 中的依赖包打包到 vendor 中。这种设计在社区也引起过几次争论,但是并没有达成一致。
  • verify: verify 用来检测依赖包自下载之后是否被改动过。
  • why: 解释为什么 package 或者 module 是需要,但是看上去解释的理由并不是非常的直观。

bash ➜ mod-demo $ go mod why github.com/astaxie/beego # github.com/astaxie/beego mod-demo github.com/astaxie/beego

3. Go 1.13 对 modules 的改动

上面在讨论 GoProxy 的时候提到了 Go 1.13 默认设置环境变量 GOPROXY 的值,除此之外 Go 1.13 对 modules 还有哪些值得注意的改动呢?

默认开启

modules 在 Go 1.13 的版本下是默认开启的。

GOPRIVATE

前面也说到对于一些内部的 package,GoProxy 并不能很好的处理,Go 1.13 推出了 GOPRIVATE 机制。只需要设置这个环境变量,然后标识出哪些 package 是 private 的,那么对于这个 package 的处理将不会从 proxy 下载。GOPRIVATE 的值是一个以逗号分隔的列表,支持正则(正则语法遵守 Golang 的 包 path.Match)。下面是一个 GOPRIVATE 的示例:

GOPRIVATE=*.corp.example.com,rsc.io/private

上面的 GOPRIVATE 表示以 *.http://corp.example.com 或者 rsc.io/private 开头的 package 都是私有的。

GOSUMDB

GOSUMDB 的全称为 Go CheckSum Database,用来下载的包的安全性校验问题。包的安全性在使用 GoProxy 之后更容易出现,比如我们引用了一个不安全的 GoProxy 之后然后下载了一个不安全的包,这个时候就出现了安全性问题。对于这种情况,可以通过 GOSUMDB 来对包的哈希值进行校验。当然如果想要关闭哈希校验,可以将 GOSUMDB 设置为 off;如果要对部分包关闭哈希校验,则可以将包的前缀设置到环境变量中 GONOSUMDB 中,设置规则类似 GOPRIVATE。

关于 GOSUMDB 的配置格式为:\<db_name>+\<publickey>+\<url>。

GOSUMDB="sum.golang.org"
GOSUMDB="sum.golang.org+<publickey>"
GOSUMDB="sum.golang.org+<publickey> https://sum.golang.org"

上面三种配置都是合理的,因为对于 sum.golang.org,Go 自己知道其对应的 publickey 和 url,所以我们只要配置一个名字即可,对于另外一个 sum.golang.google.cn 也是一样。除此之外的,都需要指明 publickey,url 默认是 https://\<db_name>。

关于 GOSUMDB 更多的详细信息可以参考:golang.org/cmd/go/#

4. 总结

Golang 包管理历经多个版本,目前来看并没有一个完全让开发者满意的方案,比如类似 Java 的 maven 的包管理方式。很多开发者也表示 modules 的管理方式也不是很直观。其实 Golang 的 package 使用 git 的管理方式来管理其实是一个很好的管理方式,我们最需要的其实如何能让普通开发者获取到任何 package 的心智负担降到最低。值得欣慰的是我们确实有看到大家在这个方向上的努力,比如 modules 的 GoProxy。

放眼未来,希望会有一种令大部分普通开发者满意的包管理方式吧。

5. Reference

  1. research.swtch.com/vgo
  2. github.com/golang/go/wi
  3. roberto.selbach.ca/intr
  4. roberto.selbach.ca/play
  5. semver.org/
  6. golang.org/cmd/go/#
  7. golang.org/doc/go1.13#
  8. goproxy.io
  9. github.com/goproxy/gopr


打个广告,欢迎关注公众号:golang-experts

编辑于 2019-10-13 17:02