Go Modules 入门教程

Go

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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://roberto.selbach.ca/intro-to-go-m...

译文地址:https://learnku.com/go/t/38809

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!