Go Modules 入门教程
Go 语言 1.11 版本 为 模块 添加实验性的新支持 , 这是一个新的 Go 依赖管理系统。几天前, 我写了一篇关于它的文章。自从文章上线以来, 事情发生了一些变化 不过我们很快就发布新版本, 我认为这是一个好时机, 用一种更实际的方法来写另一篇关于它的文章。因此我们需要做的是: 创建一个新包, 发布几个版本, 看看它们是如何工作的。
创建
首先,创建一个包取名为 「testmod」。 有一个细节需要注意: 这个目录应该在你的 $GOPATH
路径之外, 因为默认情况下, 它里面的模块支持被禁用。Go 会在第一步消除 $GOPATH
后在某个时刻完全消除。
$ mkdir testmod
$ cd testmod
我们的测试非常简单:
package testmod
import "fmt"
// 返回一个友好的问候
func Hi(name string) string {
return fmt.Sprintf("Hi, %s", name)
}
已经完成但它还不是一个模块,继续修改。
$ 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 github.com/robteix/testmod
这将获取 master
中的最新代码。这个仍然有效, 但是我们有更好的方法, 不建议继续使用该命令。从 master
获取代码 本质是危险的, 因为我们无法确定包作者是否进行了更改, 有可能破坏我们的应用,这就是模块旨在修复。
Module 版本介绍
Go Module 会区分版本,所以您将需要熟悉 语义版本化.
Go 在查找版本时会使用 Tag,而某些版本则与其他版本有所不同:例如版本 2 和更高版本的导入路径应该与版本 0 和版本 1 不同(我们将介绍该路径)。默认情况下,Go 会获取存储库中可用的最新的打过标签的版本。这一点我们需要注意,因为您可能习惯于使用 master 分支。需要记住的是,发布软件包时,我们在版本库里打标签。
制作我们的第一个版本
现在我们的包已经准备好了,我们通过使用版本标签来发布。让我们发布版本 1.0.0:
$ git tag v1.0.0
$ git push --tags
这将在我的Github存储库上创建一个 tag,将当前提交标记为发行版1.0.0。
此时一个最佳实践是同时创建新分支 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"))
}
直到现在,您可以通过获取github.com/robteix/testmod
来下载包,但是对于模块,这会变得更有趣。首先,我们需要在新程序中启用模块。
使用 Go Module 之前需要初始化:
$ go mod init mod
以上命令会创建 go.mod
文件,如如下:
module mod
此时我们使用 build
命令构建程序:
$ go build
go: finding github.com/robteix/testmod v1.0.0
go: downloading github.com/robteix/testmod v1.0.0
以上命令会自动分析项目依赖并下载,如果你打开 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=
发布一个错误修正(bugfix)
现在,假设我们意识到我们的包有一个问题:我们的 Hello 没加上标点符号!人们很生气,因为我们友好的问候不够友好。所以我们将修复它并发布一个新版本:
// Hi 返回一个友好的问候
func Hi(name string) string {
- return fmt.Sprintf("Hi, %s", name)
+ return fmt.Sprintf("Hi, %s!", name)
}
我们在 v1 分支中做了这个改变,因为它与我们稍后为 v2 做的事情无关,但是在现实生活中,也许你会在 master
中做它,然后把它备份。不管怎样,我们都需要在v1分支中进行修复,并将其标记为新版本。
$ git commit -m "增强问候的友好性" testmod.go
$ git tag v1.0.1
$ git push --tags origin v1
更新模块(modules)
默认情况下,Go不会在不被询问的情况下更新模块。这是一件好事,因为我们希望我们的构建具有可预测性。如果Go模块在每次新版本发布时都自动更新,我们就会回到Go1.11之前的不文明时代。不,我们需要告诉 Go为我们更新一个模块。
我们利用我们的老朋友来做到这一点 go get
:
- 运行
go get -u
来获取最新的 小版本或者补丁 版本。举个例子,它会将 1.0.0 更新为 1.0.1 或者 1.1.0 这样的。 - 运行
go get -u=patch
来获取最新的 patch 更新。 举个例子,它会更新到1.0.1 而不是 1.1.0 版本。 - 运行
go get package@version
来更新到一个特定的版本。(比如github.com/robteix/testmod@v1.0.1
)
在上面的列表中,似乎没有更新到最新 大版本 (major) 的方法。这是有充分理由的,我们稍后会看到。
由于我们的程序使用的是软件包的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
大版本(Major)
根据语义版本语义,大版本不同于小版本。大版本会破坏向后兼容性。从Go模块的角度来看,大版本是一个完全不同的包。起初这听起来可能很奇怪,但这是有意义的: 一个库的两个不兼容的版本,实质上是两个不同的库。
让我们来在包上做一个大(Major)改动。随着时间的推移,我们意识到我们的 API 太简单了,对于我们的用户的用例来说太有限了,所以我们需要改变Hi()
函数,添加一个新的参数:
package testmod
import (
"errors"
"fmt"
)
// Hi 返回一个 `lang` 语言的问候语
func Hi(name, lang string) (string, error) {
switch lang {
case "cn":
return fmt.Sprintf("你好,%s!", name), nil
case "en":
return fmt.Sprintf("Hi, %s!", name), nil
case "pt":
return fmt.Sprintf("Oi, %s!", name), nil
case "es":
return fmt.Sprintf("¡Hola, %s!", name), nil
case "fr":
return fmt.Sprintf("Bonjour, %s!", name), nil
default:
return "", errors.New("未知的语言")
}
}
使用我们的 API 的现有软件将会崩溃,因为它们首先不传递语言参数,其次不期望错误返回。我们的新 API 不再与1.x版本兼容,所以这里的最佳实践是将版本提升到2.0.0。
我之前提到过有些版本会有一些特点,现在就是这样。版本2 **及以上应更改导入路径。** 他们现在是不同的库。
为此,我们在模块名称的末尾添加了一个新的版本路径。
module github.com/robteix/testmod/v2
其余的和以前一样,我们推它,把它标记为v2.0.0(并可选地创建一个v2分支。)
$ git commit testmod.go -m "修改 Hi 使它支持多语言"
$ git checkout -b v2 # 并非必须创建新分支,但是推荐创建一个新的 v2 分支
$ echo "module github.com/robteix/testmod/v2" > go.mod
$ git commit go.mod -m "将版本提升到 v2"
$ git tag v2.0.0
$ git push --tags origin v2 # 如果你没有创建 v2 分支的话,使用 master 而不是 v2 分支。
更新到一个大版本(Major)
即使我们发布了新的不兼容版本的库,现有软件也不会被破坏,因为它将继续使用现有版本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
中移除依赖关系。如果您有不再使用的依赖项并且想要清理,您可以使用新的 tidy
命令:
$ go mod tidy
现在我们只剩下真正被使用的依赖项。
使用 vendor 特性
Go 模块默认忽略 vendor/
目录。这个想法是最终废除 vendor 这个特性。但是,如果我们仍然想将 vendor 依赖添加到我们的版本控制中,我们仍然可以这样做:
译者注: vendoring 指的是制作第三方依赖包的副本的行为。这些副本通常放在每个项目中,然后保存在项目仓库中。在 go 中,vendor 特性指的是你可以将依赖拷贝一份放在 vendor/ 目录下。
$ go mod vendor
这将在项目的根目录下创建一个 vendor/
目录,其中包含所有依赖项的源代码。
尽管如此,默认情况下,go build
将忽略该目录的内容。如果您想从 vendor/
目录中建立依赖关系,您需要请求它。
$ go build -mod vendor
我希望许多愿意使用 vendoring 的开发人员将在他们的开发机器上正常运行 go build
,并在他们的自动构建(CI, Continuous Integration)中使用 -mod vendor
。
这里我强调一下,Go模块正在抛弃 vendor 这个想法。有些人不希望自己的上游依赖是一个版本控制平台(例如 Github)。Go 模块为这些人提供的替代方案是使用 Go 模块代理(Go module Proxy)。
有一些方法可以保证 go 根本不会到达网络(例如,GOPROXY=off
),但这些是未来博客帖子的主题。
结论
这篇文章可能看起来有点令人畏惧,我试图将很多事情放在一起解释。现实是现在 Go 模块基本上是透明的。我们像往常一样在代码中导入包,“go”命令将处理其余部分。
当我们构建一些东西时,依赖关系将被自动获取。它还消除了使用 GOPATH
的需要,GOPATH
成为了一些新的 Go 开发者的一个障碍,因为有些新的开发者很难理解为什么我们要用一个特定的目录。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: