Go编程语言的1.11版即将发布,将为模块带来实验性支持-一种新的Go依赖管理系统。 (注意翻译:发生了发布 )
最近, 我已经写了一篇关于此的小文章 。 从那以后,有些事情发生了些微变化,并且我们离发行版越来越近了,所以在我看来,现在是时候撰写新文章了-让我们添加更多练习。
因此,这就是我们要做的事情:创建一个新程序包,然后发布一些版本以查看其工作方式。
模块创建
首先,创建我们的包。 我们称之为testmod。 重要信息: 软件包目录应放置在 $GOPATH
,因为在其中,默认情况下禁用了模块支持 。 Go模块是将来完全放弃$GOPATH
。
$ mkdir testmod $ cd testmod
我们的包很简单:
package testmod import "fmt"
该软件包已准备就绪,但尚未成为模块 。 让我们修复它。
$ go mod init github.com/robteix/testmod go: creating new go.mod: module github.com/robteix/testmod
软件包目录中有一个名为go.mod
的新文件,其内容如下:
module github.com/robteix/testmod
有点,但这就是将我们的包变成模块的原因 。
现在我们可以将此代码推送到存储库中:
$ git init $ git add * $ git commit -am "First commit" $ git push -u origin master
到目前为止,任何想使用我们软件包的人都可以申请go get
:
$ go get github.com/robteix/testmod
该命令将从master
分支中获取最新代码。 此选项仍然有效,但是如果我们不再这样做,那就更好了,因为现在“有更好的方法”。 实际上,直接从master
分支获取代码是危险的,因为我们永远无法确定软件包的作者没有做出会“破坏”我们代码的更改。 为了解决这个问题,发明了Go模块。
关于版本控制模块的一点题外话
Go模块是版本控制的,另外,各个版本也有一些特殊性。 您将必须熟悉语义版本控制的概念。
此外,Go在查找版本时会使用存储库标签,并且某些版本与其余版本有所不同:例如,版本2和更高版本的导入路径必须与版本0和版本1不同(我们将介绍到此)。
默认情况下,Go下载最新版本,该版本在存储库中具有可用的标记 。
这是一项重要功能,因为在使用master
分支时可以使用它。
对于我们来说,现在重要的是,在创建程序包的发行版时,我们需要在版本库中放置带有版本的标签。
来吧
首次发布
我们的包装已准备就绪,我们可以将其“推广”到全世界。 我们使用版本化标签来实现。 设置版本号为1.0.0:
$ git tag v1.0.0 $ git push --tags
这些命令在我的Github存储库中创建一个标记,将当前提交标记为1.0.0版。
Go并不坚持这样做,但是最好创建一个新的分支(“ v1”),我们可以将补丁发送到该分支。
$ git checkout -b v1 $ git push -u origin v1
现在,我们可以在master
分支中工作,而不必担心我们会破坏发行版。
使用我们的模块
让我们使用创建的模块。 我们将编写一个简单的程序来导入我们的新包:
package main import ( "fmt" "github.com/robteix/testmod" ) func main() { fmt.Println(testmod.Hi("roberto")) }
到现在为止,您可以运行go get github.com/robteix/testmod
来下载软件包,但是使用模块,它会变得更加有趣。 首先,我们需要在新程序中启用模块支持。
$ go mod init mod
如您所料,根据您先前阅读的内容,目录go.mod
出现一个新的go.mod
文件,其中的模块名称为:
module mod
当我们尝试将程序组合在一起时,情况变得更加有趣:
$ go build go: finding github.com/robteix/testmod v1.0.0 go: downloading github.com/robteix/testmod v1.0.0
如您所见, go
命令自动找到并下载了我们程序导入的包。
如果我们检查go.mod
文件,我们将看到某些变化:
module mod require github.com/robteix/testmod v1.0.0
我们得到了另一个名为go.sum
新文件,其中包含用于检查正确版本和文件的软件包的哈希值。
github.com/robteix/testmod v1.0.0 h1:9EdH0EArQ/rkpss9Tj8gUnwx3w5p0jkzJrd5tRAhxnA= github.com/robteix/testmod v1.0.0/go.mod h1:UVhi5McON9ZLc5kl5iN2bTXlL6ylcxE9VInV71RrlO8=
制作错误修复版本
现在,假设我们在程序包中发现了一个问题:问候语中没有标点符号!
有些人会大怒,因为我们的友好问候已经不再那么友好了。
让我们修复此问题并发布新版本:
// Hi returns a friendly greeting func Hi(name string) string { - return fmt.Sprintf("Hi, %s", name) + return fmt.Sprintf("Hi, %s!", name) }
我们直接在v1
分支中进行了此更改,因为它与我们接下来在v2
分支中将要进行的操作无关,但是在现实生活中,也许您应该在master
进行这些更改,然后将它们反向移植到v1
。 无论如何,此修复程序应位于v1
分支中,我们需要将其标记为新版本。
$ git commit -m "Emphasize our friendliness" testmod.go $ git tag v1.0.1 $ git push --tags origin v1
模块更新
默认情况下,Go不会在没有需求的情况下更新模块。 “那很好,”因为我们都希望构建的可预测性。 如果每次发布新版本时Go模块都会自动更新,我们将返回“ Go1.11之前的黑暗时代”。 但是不,我们需要告诉 Go为我们更新模块。
我们将在我们的老朋友的帮助下做到这一点go get
:
运行go get -u
以使用最新的次要版本或补丁程序版本(即,如果该版本可用,则命令将从1.0.0更新为1.0.1或更新为1.1.0)。
运行go get -u=patch
以使用最新的修补程序版本(即,该软件包将更新为1.0.1,但不会更新为1.1.0)
运行go get package@version
升级到特定版本(例如, github.com/robteix/testmod@v1.0.1
)
此列表中没有办法升级到最新的主要版本。 我们很快就会看到,这样做有充分的理由。
由于我们的程序使用了软件包的1.0.0版本,并且我们刚刚创建了1.0.1版本,因此以下任何命令会将我们更新为1.0.1:
$ go get -u $ go get -u=patch $ go get github.com/robteix/testmod@v1.0.1
启动后(假设go get -u
),我们的go.mod
已更改:
module mod require github.com/robteix/testmod v1.0.1
主要版本
根据语义版本控制的规范,主要版本与次要版本不同 。 主要版本可能会破坏向后兼容性。 从Go模块的角度来看,主要版本是一个完全不同的包 。
乍一看听起来很疯狂,但这是有道理的:两个不兼容的库版本是两个不同的库。
让我们对程序包进行重大更改。 假设随着时间的流逝,我们已经很清楚我们的API太简单了,对于我们用户的用例来说太局限了,因此我们需要更改Hi()
函数以接受欢迎语言作为参数:
package testmod import ( "errors" "fmt" )
使用我们的API的现有程序会中断,因为它们a)不会将语言作为参数传递,并且b)不会返回错误。 我们的新API不再与1.x版兼容,因此请符合2.0.0版。
我之前提到过某些版本具有功能,现在是这种情况。
版本2 或更高版本应更改导入路径。 现在这些是不同的库。
我们将通过在模块名称中添加新版本的路径来做到这一点。
module github.com/robteix/testmod/v2
其他所有内容都相同:推送并放置一个标签,该标签为v2.0.0(并有选择地添加分支v2)
$ git commit testmod.go -m $ git checkout -b v2 # optional but recommended $ echo > go.mod $ git commit go.mod -m $ git tag v2.0.0 $ git push --tags origin v2 # or master if we don't have a branch
主要版本更新
即使我们发布了库的新不兼容版本,现有程序也没有中断 ,因为它们继续使用版本1.0.1。
go get -u
将不会下载版本2.0.0。
但是在某个时候,我(作为库用户)可能希望升级到2.0.0版本,因为例如,我是需要多种语言支持的用户之一。
要更新,我需要相应地更改程序:
package main import ( "fmt" "github.com/robteix/testmod/v2" ) func main() { g, err := testmod.Hi("Roberto", "pt") if err != nil { panic(err) } fmt.Println(g) }
现在,当我运行go build
,它将“退出”并为我下载2.0.0版。 请注意,尽管导入路径现在以“ v2”结尾,但Go仍以其真实名称(“ testmod”)引用该模块。
正如我所说,主要版本在各个方面都是不同的软件包。 这两个Go模块没有以任何方式连接。 这意味着我们可以在一个二进制文件中有两个不兼容的版本:
package main import ( "fmt" "github.com/robteix/testmod" testmodML "github.com/robteix/testmod/v2" ) func main() { fmt.Println(testmod.Hi("Roberto")) g, err := testmodML.Hi("Roberto", "pt") if err != nil { panic(err) } fmt.Println(g) }
当依赖项依赖于同一库的不同版本时,这消除了依赖项管理的常见问题。
我们把事情整理好
让我们回到以前的版本,该版本仅使用testmod 2.0.0-如果现在检查go.mod
的内容,我们会注意到:
module mod require github.com/robteix/testmod v1.0.1 require github.com/robteix/testmod/v2 v2.0.0
默认情况下,除非您要求,Go不会从go.mod
删除依赖go.mod
。 如果您有不再需要的依赖项并且想要清除它们,则可以使用新的tidy
命令:
$ go mod tidy
现在,我们只有真正使用的依赖项。
贩卖
默认情况下,转到模块忽略vendor/
目录。 这个想法是逐步摆脱自动贩卖1 。 但是,如果我们仍然想将“分离的”依赖项添加到我们的版本控制中,则可以执行以下操作:
$ go mod vendor
团队将在我们项目的根目录下创建vendor/
目录,其中包含所有依赖项的源代码。
但是,默认情况下, go build
仍会忽略此目录的内容。 如果要从vendor/
目录收集依赖关系,则必须明确要求它。
$ go build -mod vendor
我假设许多想使用自动售货的开发人员将像往常一样在其计算机上运行go build
并在其CI上使用-mod vendor
。
再次,Go模块正在从自动售货的想法转变为对那些不想直接依赖上游版本控制服务的用户使用代理作为模块。
有多种方法可以确保无法使用go
网络(例如,使用GOPROXY=off
),但这是下一篇文章的主题。
结论
对于某些人来说,这篇文章可能看起来很复杂,但这是因为我试图一次解释很多。 现实是,Go模块今天通常很简单-我们像往常一样将包导入到我们的代码中,而go
团队则为我们完成其余的工作。 在组装过程中将自动加载依赖项。
这些模块还消除了对$GOPATH
的需要,这对于新的Go开发人员是一个绊脚石,他们在理解为什么要在特定目录中放置内容时遇到了麻烦。
自动售货(非正式地)已被弃用,以支持使用代理。 1个
我可以撰写有关Go模块代理的单独文章。
注意事项:
1我认为这是一个太大声的表达,有些人可能会觉得现在正在取消自动售货机。 事实并非如此。 自动售货仍在起作用,尽管与以前略有不同。 显然,人们希望用更好的东西来代替自动售货机,例如代理(不是事实)。 到目前为止,这仅仅是追求更好的解决方案。 除非找到合适的替代品(如果有),自动售货就不会消失。