美文网首页
【译】模块(Modules)第三部分:最小版本选择

【译】模块(Modules)第三部分:最小版本选择

作者: 豆腐匠 | 来源:发表于2020-04-25 16:01 被阅读0次

    系列索引

    为什么以及什么
    项目,依赖项和Gopls
    最小版本选择
    镜像,校验和和Athens
    Gopls改进
    vendor使用

    介绍

    每个依赖管理解决方案都必须解决选择依赖版本的问题。当今存在的众多版本选择算法都试图针对任何依赖都识别出“最新最大”的版本。如果你认为语义版本控制能够正确应用并且都遵守公认的法则,那么这是有道理的。在这些情况下,依赖项的“最新最大”的版本应该是最稳定和安全的版本,并且应与较早版本具有向后兼容性。至少在相同的主版本依赖树中可以做到。

    Go决定采用其他方法,Russ Cox花费了大量时间和精力撰写谈论 Go团队的版本选择方法,即最小版本选择或MVS。从本质上讲,Go团队相信MVS可以为Go程序提供最佳的方案,以实现长期的持久性和可复制性。我建议阅读这篇文章以了解Go团队为什么相信这一点。

    在本文中,我将尽力解释MVS语义,并展示Go和MVS算法实际应用的示例。

    MVS语义

    将Go的选择算法命名为“最小版本选择”有点用词不当,但是一旦了解了它的工作原理,就会发现名称真的很贴切。如前所述,许多选择算法会选择依赖项的“最新最大”版本。我喜欢将MVS视为选择“最新非最大”版本的算法。并不是说MVS不能选择“最新最大”,而是只要项目中的任何依赖项都不需要“最新最大”,该版本就没有存在的必要。

    为了更好地理解这一点,让我们创建一种场景,其中几个模块(A,B和C)依赖于同一模块(D),但是每个模块都需要不同的版本。

    图1

    image

    图1显示了模块A,B和C如何分别独立地需要模块D和每一个都需要不同版本的模块。

    如果我启动一个需要模块A的项目,那么为了构建代码,我还需要模块D。可能会有许多版本的D模块可供选择。例如,假设模块D代表sirupsen的logrus模块。我可以要求Go向我提供模块D标记的所有版本的列表。

    代码2

    $ go list -m -versions github.com/sirupsen/logrus
    
    github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0
    v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1
    v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 
    v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4
    v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1
    v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3
    v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0
    v1.4.0 v1.4.1 v1.4.2
    
    

    代码2显示了模块D存在的所有版本,其中显示了“最新最大”版本为v1.4.2。

    该项目应选择哪个版本的模块D?有两种选择。第一个是选择“最新的”版本(在主要版本1的这一行中),即v1.4.2。第二个选择是选择模块A所需的版本v1.0.6。

    像dep这样的依赖工具将选择v1.4.2版,并在语义版本控制和通用规范得到尊重的前提下工作。但是,对于Russ在这个文章中定义的原因,Go会尊重模块A的要求并选择版本1.0.6。Go从这个需要模块的项目中的所有依赖项选择了当前在所需版本“最小”版本。换句话说,现在只有模块A需要模块D,而模块A已指定它需要版本v1.0.6,因此选择了模块D的1.0.6版本。

    如果我引入了新代码,要求项目导入模块B怎么办?将模块B导入项目后,Go会将项目的模块D版本从v1.0.6升级到v.1.2.0。再次从项目中需要模块D的所有依赖项(模块A和B)选择了的“最小”版本,该版本当前处于所需版本集(v1.0.6和v.1.2.0)中。

    如果我再次引入新的代码,需要项目导入模块C的怎么办?那么,Go将从所需版本集(v1.0.6,v1.2.0,v1.3.2)中选择最新版本(v1.3.2)。请注意,版本v1.3.2仍然是模块D(v1.4.2)的“最小”版本,而不是“最新的最大”版本。

    最后,如果删除刚刚为模块C添加的代码,该怎么办?Go会将项目锁定到模块D的v1.3.2中。降级到版本v1.2.0将是一个巨大的更改,而Go知道版本v1.3.2可以正常运行并且稳定,因此版本v1.3.2仍然是模块D的“最新的非最大”或“最小”版本。另外,模块文件仅维护快照,而不是日志,没有用于历史撤消或降级的信息。

    这就是为什么我喜欢将MVS视为选择模块“最新非最大”版本的原因。希望您现在能理解为什么Russ在命名算法时选择名称“ minimal”。

    示例项目

    有了这个基础,我将整理一个项目,以便你可以看到Go如何实际使用MVS算法。在此项目中,模块D将代表logrus模块,而该项目将直接依赖于rethinkdb-go(模块A)和golib(模块B)模块。rethinkdb-go和golib模块直接依赖logrus模块,并且每个模块都需要一个不同的版本,该版本不是logrus的“最新”版本。

    图2

    image

    图2显示了三个模块之间的独立关系。首先,我将创建项目,初始化模块,然后加载VS Code。

    代码2

    $ cd $HOME
    $ mkdir app
    $ mkdir app/cmd
    $ mkdir app/cmd/db
    $ touch app/cmd/db/main.go
    $ cd app
    $ go mod init app
    $ code .
    
    

    代码2显示了所有要运行的命令。运行这些命令后,VS Code中应该会出现下面的效果。

    图3

    image

    图3显示了项目结构和模块文件应包含的内容。有了这个,现在该添加使用rethinkdb-go模块的代码了。
    代码3
    https://play.golang.org/p/bc5I0Afxhvc

    01 package main
    02
    03 import (
    04     "context"
    05     "log"
    06
    07     db "gopkg.in/rethinkdb/rethinkdb-go.v5"
    08 )
    09
    10 func main() {
    11     c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil)
    12     if err != nil {
    13         log.Fatalln(err)
    14     }
    15
    16     if _, err = c.Query(context.Background(), db.Query{}); err != nil {
    17         log.Fatalln(err)
    18     }
    19 }
    
    

    代码3引入了rethinkdb-go模块的v5版本。添加并保存此代码后,Go会查找,下载和提取模块,并更新go.modgo.sum文件。

    代码4

    01 module app
    02
    03 go 1.13
    04
    05 require gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
    

    代码4显示了go.mod选择版本v5.0.1的rethinkdb-go模块作为直接依赖,该版本是该模块的“最新最大版本”。

    代码5

    github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
    github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
    

    代码5显示go.sum中介绍logrus模块v1.0.6版本的两行。此时,可以看到MVS算法已经选择了满足rethinkdb-go模块指定要求的所需的logrus模块的“最小”版本。记住logrus模块的“最新最大”版本是1.4.2。

    注意:go.sum文件应被视为不透明的可靠性工件,并且不应该用于理解依赖关系。我在上面所做的确定版本的操作是错误的,稍后我将向你展示确定项目所使用的版本的正确方法。

    图4

    image

    图4显示了Go在构建代码时将使用哪个版本的logrus模块。

    接下来,我将添加引入对golib模块存在依赖关系的代码。

    代码6
    https://play.golang.org/p/h23opcp5qd0

    01 package main
    02
    03 import (
    04     "context"
    05     "log"
    06
    07     "github.com/Bhinneka/golib"
    08     db "gopkg.in/rethinkdb/rethinkdb-go.v5"
    09 )
    10
    11 func main() {
    12     c, err := db.NewCluster([]db.Host{{Name: "localhost", Port: 3000}}, nil)
    13     if err != nil {
    14         log.Fatalln(err)
    15     }
    16
    17     if _, err = c.Query(context.Background(), db.Query{}); err != nil {
    18         log.Fatalln(err)
    19     }
    20
    21     golib.CreateDBConnection("")
    22 }
    
    

    代码6添加了07和21行。Go查找,下载并解压缩golib模块后,以下更改将显示在go.mod文件中。

    代码7

    01 module app
    02
    03 go 1.13
    04
    05 require (
    06     github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba
    07     gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
    08 )
    

    代码7显示go.mod文件已被修改为包括golib模块的“最新最大”版本,该版本恰好没有语义版本标签。

    代码8

    ...
    github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s=
    github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
    github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
    github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
    ...
    
    

    代码8显示了go.sum文件中的四行,现在包括logrus模块的v1.0.6和v1.2.0版本。看到go.sum文件中列出的两个版本会带来两个问题:

    1. 为什么在go.sum文件中列出了两个版本?
    2. Go执行构建时将使用哪个版本?

    Go小组的Bryan Mills可以很好地回答go.sum文件中列出两个版本的原因。

    “ go.sum文件仍包含旧版本(1.0.6),因为其传递依赖可能会影响其他模块的选定版本。我们真的只需要go.mod文件的校验和,因为那是声明这些传递依赖的内容的地方,但是由于go mod tidy不够精确,最终我们也保留了源代码的校验和。”
    golang.org/issue/33008

    在构建项目时将使用哪个版本的logrus模块?现在仍然存在这样的问题。要正确识别将使用哪些模块及其版本,请不要查看该go.sum文件,而应使用go list命令。

    代码9

    $ go list -m all | grep logrus
    
    github.com/sirupsen/logrus v1.2.0
    

    代码9显示了在构建项目时将使用logrus模块的v1.2.0版本。go list 的-m标签展示出的列表是模块而不是包。

    查看模块图将更深入地了解项目对logrus模块的要求。

    代码10

    $ go mod graph | grep logrus
    
    github.com/sirupsen/logrus@v1.2.0 github.com/pmezard/go-difflib@v1.0.0
    github.com/sirupsen/logrus@v1.2.0 github.com/stretchr/objx@v0.1.1
    github.com/sirupsen/logrus@v1.2.0 github.com/stretchr/testify@v1.2.2
    github.com/sirupsen/logrus@v1.2.0 golang.org/x/crypto@v0.0.0-20180904163835-0709b304e793
    github.com/sirupsen/logrus@v1.2.0 golang.org/x/sys@v0.0.0-20180905080454-ebe1bf3edb33
    gopkg.in/rethinkdb/rethinkdb-go.v5@v5.0.1 github.com/sirupsen/logrus@v1.0.6
    github.com/sirupsen/logrus@v1.2.0 github.com/konsorten/go-windows-terminal-sequences@v1.0.1
    github.com/sirupsen/logrus@v1.2.0 github.com/davecgh/go-spew@v1.1.1
    github.com/Bhinneka/golib@v0.0.0-20191209103129-1dc569916cba github.com/sirupsen/logrus@v1.2.0
    github.com/prometheus/common@v0.2.0 github.com/sirupsen/logrus@v1.2.0
    

    代码10显示了logrus模块在项目中的关系。这里直接提取了对logrus的依赖关系的行。

    代码11

    gopkg.in/rethinkdb/rethinkdb-go.v5@v5.0.1 github.com/sirupsen/logrus@v1.0.6
    github.com/Bhinneka/golib@v0.0.0-20191209103129-1dc569916cba github.com/sirupsen/logrus@v1.2.0
    github.com/prometheus/common@v0.2.0 github.com/sirupsen/logrus@v1.2.0
    

    在代码11中,这些行显示三个模块(rethinkdb-go,golib和common)都需要logrus模块。由于有了go list命令,我知道所需的最低版本为v1.2.0。

    图5

    image.png

    图5展示了Go现在将使用哪个版本的logrus模块来为所有需要logrus模块的依赖项提供服务。

    Go Mod Tidy

    在将代码commit/push到存储库之前,请运行go mod tidy以确保模块文件是最新且准确的。在本地构建,运行或测试的代码将影响Go在模块文件中进行更新的内容。运行go mod tidy将确保项目具有所需内容的准确和完整的快照,这有利于团队中的其他人和你的CI / CD环境。

    代码12

    $ go mod tidy
    
    go: finding github.com/Bhinneka/golib latest
    go: finding github.com/bitly/go-hostpool latest
    go: finding github.com/bmizerany/assert latest
    

    代码12显示了执行go mod tidy的输出。你会在输出中看到两个新的依赖项。这将更改模块文件。

    代码13

    01 module app
    02
    03 go 1.13
    04
    05 require (
    06     github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba
    07     github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
    08     github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
    09     gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
    10 )
    

    代码13显示了go-hostpool和assert模块被列为构建项目所需的间接依赖模块。之所以在此处列出它们,是因为这些项目当前与模块不兼容。换句话说,这些项目的任何标记版本或master中“最新最大”版本中的go.mod文件都不存在。

    为什么在运行go mod tidy会后包含这些模块?我可以使用go mod why命令找出答案。

    代码14

    $ go mod why github.com/hailocab/go-hostpool
    
    # github.com/hailocab/go-hostpool
    app/cmd/db
    gopkg.in/rethinkdb/rethinkdb-go.v5
    github.com/hailocab/go-hostpool
    
    ------------------------------------------------
    
    $ go mod why github.com/bmizerany/assert
    
    # github.com/bmizerany/assert
    app/cmd/db
    gopkg.in/rethinkdb/rethinkdb-go.v5
    github.com/hailocab/go-hostpool
    github.com/hailocab/go-hostpool.test
    github.com/bmizerany/assert
    

    代码14显示了为什么项目间接依赖这些模块。rethinkdb-go模块依赖go-hostpool模块,而go-hostpool模块依赖assert模块。

    升级依赖关系

    该项目具有三个依赖项,每个依赖项都需要logrus模块,当前选择了logrus模块的v1.2.0版本。在项目生命周期的某个时刻,升级直接和间接依赖关系以确保项目所需的代码是最新的并且可以使用新功能,错误修复和安全补丁将变得很重要。要升级依赖,Go提供了go get命令。

    在运行go get升级项目的依赖项之前,需要考虑几件事。

    使用MVS仅升级必需的直接和间接依赖项

    我建议从这种升级开始,直到你了解有关项目和模块的更多信息。go get是的最保守的形式。

    代码15

    $ go get -t -d -v ./...
    

    代码15显示了如何使用MVS算法执行仅关注所需依赖项的升级。下面是标签的定义。

    • -t flag:考虑构建测试所需的模块。
    • -d flag:下载每个模块的源代码,但不要构建或安装它们。
    • -v flag:提供详细输出。
    • ./... :在整个源代码树中执行这些操作,并且仅更新所需的依赖项。

    对当前项目运行此命令不会导致任何更改,因为该项目已经是最新版本,并且具有构建和测试该项目所需的最低版本。那是因为我刚运行go mod tidy,项目是新的。

    使用“最新最大”升级仅升级必需的直接和间接依赖项

    这种升级将使整个项目的依赖性从“最小”版本增加到“最新最大”版本。所需要做的只是将-u添加到命令行。

    代码16

    $ go get -u -t -d -v ./...
    
    go: finding golang.org/x/net latest
    go: finding golang.org/x/sys latest
    go: finding github.com/hailocab/go-hostpool latest
    go: finding golang.org/x/crypto latest
    go: finding github.com/google/jsonapi latest
    go: finding gopkg.in/bsm/ratelimit.v1 latest
    go: finding github.com/Bhinneka/golib latest
    

    代码16显示了运行go get带有-u标志的命令的输出。此输出无法说明真实情况。如果我问go list命令现在使用哪个版本的logrus模块来构建项目,会发生什么情况?

    代码17

    $ go list -m all | grep logrus
    
    github.com/sirupsen/logrus v1.4.2
    

    代码17显示了现在如何选择“最新最大”的logrus。为了使这一选择更加明确,对go.mod文件进行了更改。

    代码18

    01 module app
    02
    03 go 1.13
    04
    05 require (
    06     github.com/Bhinneka/golib v0.0.0-20191209103129-1dc569916cba
    07     github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 // indirect
    08     github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
    09     github.com/cenkalti/backoff v2.2.1+incompatible // indirect
    10     github.com/golang/protobuf v1.3.2 // indirect
    11     github.com/jinzhu/gorm v1.9.11 // indirect
    12     github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
    13     github.com/sirupsen/logrus v1.4.2 // indirect
    14     golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect
    15     golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
    16     golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect
    17     gopkg.in/rethinkdb/rethinkdb-go.v5 v5.0.1
    18 )
    

    代码18在第13行显示版本v1.4.2现在是项目中logrus模块的选定版本。在构建项目时,Go会注意模块文件中的这一行。即使删除了这行依赖关系的代码,该项目的v1.4.2版现在也已锁定。请记住,降级将是一个更大的变化,而v.1.4.2版将不受影响。

    在go.sum文件中可以看到什么变化?

    清单19

    github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
    github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
    github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
    github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
    github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
    
    

    代码19显示了如何在go.sum文件中表示logrus的所有三个版本。正如上面的Bryan所解释的,这是因为传递依赖可能会影响其他模块的选定版本。

    图6

    image

    图6展示了Go现在将使用哪个版本的logrus模块来进行构建代码。

    使用“最新最大”版本升级所有直接和间接依赖项

    你可以all来替代./... 升级,包括所有直接和间接依赖项,以及包括不需要构建项目的依赖项。

    代码20

    $ go get -u -t -d -v all
    
    go: downloading github.com/mattn/go-sqlite3 v1.11.0
    go: extracting github.com/mattn/go-sqlite3 v1.11.0
    go: finding github.com/bitly/go-hostpool latest
    go: finding github.com/denisenkom/go-mssqldb latest
    go: finding github.com/hailocab/go-hostpool latest
    go: finding gopkg.in/bsm/ratelimit.v1 latest
    go: finding github.com/google/jsonapi latest
    go: finding golang.org/x/net latest
    go: finding github.com/Bhinneka/golib latest
    go: finding golang.org/x/crypto latest
    go: finding gopkg.in/tomb.v1 latest
    go: finding github.com/bmizerany/assert latest
    go: finding github.com/erikstmartin/go-testdb latest
    go: finding gopkg.in/check.v1 latest
    go: finding golang.org/x/sys latest
    go: finding github.com/golang-sql/civil latest
    

    代码20显示了现在为该项目找到,下载和提取了多少个依赖项。

    代码21

    Added to Module File
       cloud.google.com/go v0.49.0 // indirect
       github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect
       github.com/google/go-cmp v0.3.1 // indirect
       github.com/jinzhu/now v1.1.1 // indirect
       github.com/lib/pq v1.2.0 // indirect
       github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
       github.com/onsi/ginkgo v1.10.3 // indirect
       github.com/onsi/gomega v1.7.1 // indirect
       github.com/stretchr/objx v0.2.0 // indirect
       google.golang.org/appengine v1.6.5 // indirect
       gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
       gopkg.in/yaml.v2 v2.2.7 // indirect
    
    Removed from Module File
       github.com/golang/protobuf v1.3.2 // indirect
    

    代码21显示了对该go.mod文件的更改。添加了更多模块,并删除了一个模块。

    注意:go mod vendor命令将从vendor文件夹中剥离测试文件。

    作为一般准则,在使用go get升级用于项目的依赖项时,请勿使用all或-u标签。坚持只使用需要的模块,并使用MVS算法选择这些模块及其版本。必要时手动覆盖特定的模块版本。手动覆盖可以通过手动编辑go.mod文件来完成,我将在以后的文章中展示。

    重置依赖关系

    如果你在任何时候不满意所选的模块和版本,可以通过删除模块文件并再次执行go mod tidy来重置选择。当项目比较新并且情况不稳定时,这也是一种选择。项目稳定并发布后,对是否要重新设置依赖关系我持怀疑态度。正如我上面提到的,随着时间的推移,可能会设置模块的版本,同时我们也需要长期持久且可复制的构建。

    代码22

    $ rm go.*
    $ go mod init <module name>
    $ go mod tidy
    

    我们可以运行代码22显示的命令,来使用MVS从头开始再次执行所有选择。在撰写本文的整个过程中,我一直在进行此操作以重置项目以此提供写本篇文章的素材。

    结论

    在这篇文章中,我解释了MVS语义,并展示了Go和MVS算法实际应用的真实示例。我还展示了一些Go命令,这些命令可以在你遇到问题或遇到未知问题时为你提供信息。在为项目添加越来越多的依赖项时,可能会遇到一些极端情况。这是因为Go生态系统已有10年的历史,所有现有项目都需要更多时间才能符合模块要求。

    在以后的文章中,我将讨论在同一项目中使用不同主要版本的依赖关系,以及如何手动检索和锁定依赖关系的特定版本。就目前而言,我希望你可以更加信任模块和Go工具,并且你对MVS如何随时间选择版本有了更清晰的了解。如果你遇到任何问题,可以在Gopher Slack的#module组的上找到一群愿意提供帮助的人。

    相关文章

      网友评论

          本文标题:【译】模块(Modules)第三部分:最小版本选择

          本文链接:https://www.haomeiwen.com/subject/wdzmbhtx.html