模块是 golang 生态提出的用于解决代码依赖和版本问题的方案。今天就来看一下,模块的内部工作原理以及如何使用模块。

在 golang 中,模块的定义为:

一个包的集合。这个包一起发布,具有相同的版本号。模块可以直接从版本管理仓库中下载或者从模块代理服务器下载。

在分析 golang 的模块功能之前,我们先根据日常的经验,总结一下,解决代码依赖和版本问题,需要做哪些工作:

  • • 如何唯一标识软件包。如果你要使用一个软件包,你一定要知道它在哪里,叫什么名字。

  • • 如何指定一个软件包的版本。在软件开发中,各个软件包都会以自己的研发进度独立发布,每一次发布都可以看成是一个版本。当你依赖一个软件包时,其实你是依赖的它的某一个版本,你在这个版本上开发、测试。版本的变更可能导致你的代码无法编译、引入 BUG等,因此你需要能够指定依赖的代码包版本,并显式的进行升级或降级

好了,接下来,将围绕上述两个问题,来看 golang 的模块功能是如何处理的。

模块标识

go.mod
module github.com/go-redis/redis/v9

go 1.17

require (
    github.com/cespare/xxhash/v2 v2.1.2
    github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
    github.com/onsi/ginkgo v1.16.5
    github.com/onsi/gomega v1.19.0
)

require (
    github.com/fsnotify/fsnotify v1.4.9 // indirect
    github.com/nxadm/tail v1.4.8 // indirect
    golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
    golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
    golang.org/x/text v0.3.7 // indirect
    gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
    gopkg.in/yaml.v2 v2.4.0 // indirect
)

module 指令

module 指令用于定义模块路径。官方文档中,对模块路径做了如下解释:

  1. 1. 模块路径应该描述模块的功能,同时告知模块的位置。

  2. 2. 模块路径一般由:仓库根目录+模块在仓库中的路径+主版本号(仅在主版本号大于等于2时)。以上面的为例:

    1. 1. 仓库根目录:github.com/go-redis/redis。

    2. 2. 模块在仓库中的路径:/。

    3. 3. 主版本号:v9。

可以看到,模块路径中已经包含了版本信息了,因此我们可以通过模块路径指定使用的模块版本。

版本号

vx.y.zpostfix
  1. 1. x:主版本号(major version)。当模块的公开接口发生了不兼容修改时,应该递增主版本号,同时y、z重新从0开始计数。

  2. 2. y:次版本号(minor version)。当公开接口未发生不兼容修改,功能发生了增加时,应该递增次版本号,同时z重新从0开始计数。

  3. 3. z:补丁版本号(patch version)。当做兼容的 BUG 修复时,应该递增补丁版本号。

  4. 4. postfix:后缀用于指定版本的特性,目前有两种后缀:

    1. 1. -pre:指定版本为预发布版本。预发布版本的版本号低于对应的正常版本号。

    2. 2. 其它:不作为版本比较的字段。可以用于表示额外信息,不如发布的阶段等。

-
pseudo-verionv0.0.0-20191109021931-daa7c04131f5
  1. 1. v0.0.0-:固定串。还可以是其它形式,用处不大,这里不介绍。

  2. 2. 20191109021931:时间,对应后面commit id的提交时间。

  3. 3. daa7c04131f5:commit id。

go get -d golang.org/x/net@daa7c041

如何将版本号映射到 commit

基于上述的版本号规则,需要唯一映射到仓库中的一个 commit。根据主目录是否在仓库的根目录下,分情况映射:

gopls/v2.4.0golang.org/x/tools/goplsv2.4.0

兼容性处理

一个成熟的依赖解决方案,需要处理兼容性问题。在如下场景下需要做兼容处理:

go.mod

下面分情况进行说明。

代码库不是模块

当被引用的代码库不是模块时,可以直接通过commit id来生成伪版本号进行引用,此时代码库不支持不同版本同时使用。

引用的代码库发生了不兼容修改

模块对于代码不兼容的处理的思路是:将他们视为两个代码库,程序中可以同时使用

在版本号那一节,我们介绍了版本号的规范,其中提到:当进行了不兼容的修改时,主版本号+1,同时次版本号和补丁版本号从0开始计数。当主版本号增加之后,我们可以采用如下的方式使用不同版本的代码库:

package main

import (
    "go.etcd.io/etcd/client/v2"
    etcd3 "go.etcd.io/etcd/client/v3"
    "go.etcd.io/etcd/client/v3/clientv3util"
    "time"
)

func main() {
    etcd3.New(etcd3.Config{
        Endpoints:   []string{"http://254.0.0.1:12345"},
        DialTimeout: 2 * time.Second,
    })

    client.New(client.Config{
        Endpoints: []string{"http://254.0.0.1:12345"},
    })

    clientv3util.KeyExists("")
}

go get go.etcd.io/etcd/client/v2

这样,我们就可以在代码中同时使用不同主版本的代码库,这为平滑迁移带来了便利,你不在需要为了升级代码库,而去修改整个项目

可以发现,通过这种方式,可能会出现在项目中,同一个仓库的代码的多个版本再长时间内并存,这对于代码的维护同样也是负担,因此最好再团队中对代码库的不同版本同时使用进行规范,在一个可预见的时间内,废弃旧版本。

依赖管理

查看依赖项

要管理依赖,首先需要知道依赖的请求,通过在模块的目录下执行如下命令,可以查看有哪些代码库、模块可以升级:

$ go list -u -m all
go: downloading github.com/gofrs/uuid v1.2.0
go: downloading github.com/golang-jwt/jwt v1.0.2
go: downloading github.com/mattn/go-sqlite3 v1.14.14
myproject
cloud.google.com/go v0.65.0 [v0.103.0]
cloud.google.com/go/bigquery v1.8.0 [v1.36.0]
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 [v0.0.0-20210121224620-deaf085860bc]
github.com/Masterminds/semver/v3 v3.1.1
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/antihax/optional v1.0.0
github.com/coreos/go-systemd/v22 v22.3.2
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 [v0.10.3]
github.com/gofrs/uuid v4.0.0+incompatible [v4.2.0+incompatible]
github.com/golang-jwt/jwt v3.2.2+incompatible

添加依赖项

go.mod
# 添加etcd v3 版本代码库依赖
$go get go.etcd.io/etcd/client/v3
# 添加etcd v2 版本代码库依赖。还记得前面有介绍,可以同时使用一个库的多个版本吗?
$go get go.etcd.io/etcd/client/v2

对于不符合模块规范的代码库,可以通过commit id直接添加依赖:

# 通过指定 commit id 添加 tidb 的 parser 作为依赖模块
$go get -v github.com/pingcap/tidb/parser@d6be910

执行该命令后,go.mod 的内容变化为:

// go.mod
@@ -6,6 +6,7 @@ require (
        github.com/go-redis/redis/v7 v7.4.1
        github.com/go-redis/redis/v8 v8.11.5
        github.com/go-redis/redis/v9 v9.0.0-beta.1
+       github.com/pingcap/tidb/parser v0.0.0-20220627062839-d6be9105e6c4 // indirect
        go.etcd.io/etcd/client/v2 v2.305.4 // indirect
        go.etcd.io/etcd/client/v3 v3.5.4 // indirect
 )

tidb 的 parser 模块根目录下虽然有go.mod文件,但是并不存在符合要求的版本号。因此,为其生成了伪版本号。查看 commit 对应的提交信息:

$ git show remote -v
commit d6be9105e6c40e6f82b8ee38e88c02d87664195a (tag: v5.4.2)
Author: ti-srebot <66930949+ti-srebot@users.noreply.github.com>
Date:   Mon Jun 27 14:28:39 2022 +0800

    lightning: split and scatter regions in batches (#33625) (#34258)

    close pingcap/tidb#33618

可以发现,伪版本号中间的时间为时区0对应的时间的字符串表示。

依赖项降级

如果发生依赖项有bug,则可能需要降级。对于不按模块规范来的代码库,降级就是重新添加对应commit的依赖项,降级后,go.mod文件的变化为:

模块依赖降级对比

可以发现,降级就是将以前的依赖进行了修改。

同样,升级操作也一样,就不过多赘述了。

结束语

以上就是使用模块需要了解的一些基本知识,模块还有其它一些处理细节,文本就不一一介绍了,感兴趣的可以阅读文末的参考文献。

参考文献

  1. 1. Go Modules Reference. https://go.dev/ref/mod#modules-overview