当前位置: 首页 > 工具软件 > GO-YEA > 使用案例 >

Part 3 —— 发布 Go Modules

汤乐家
2023-12-01

简介(Introduction)

翻译自 Go 官方博文 Publishing Go Modules

Jean de Klerk
21 August 2019

这篇文章是系列文章的第三部分。

这篇文章讨论了如何编写和发布模块,以便其他模块可以依赖它们。

请注意:这篇文章涵盖了 v1 及其之前的开发,如果您对 v2 感兴趣,请参阅 Go Modules: v2 and Beyond

这篇文章在例子中使用了 Git,但 MercurialBazaar 等也得到了支持。

项目设置(Project setup)

对于这篇文章,您需要一个现有的项目作为示例。因此,从 Using Go Modules 文章末尾的文件开始:

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote/v3 v3.1.0

$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

$ cat hello.go
package hello

import "rsc.io/quote/v3"

func Hello() string {
    return quote.HelloV3()
}

func Proverb() string {
    return quote.Concurrency()
}

$ cat hello_test.go
package hello

import (
    "testing"
)

func TestHello(t *testing.T) {
    want := "Hello, world."
    if got := Hello(); got != want {
        t.Errorf("Hello() = %q, want %q", got, want)
    }
}

func TestProverb(t *testing.T) {
    want := "Concurrency is not parallelism."
    if got := Proverb(); got != want {
        t.Errorf("Proverb() = %q, want %q", got, want)
    }
}

$

接下来,创建一个新的 git 存储仓库并添加一个初始提交。如果您正在发布您自己的项目,请确保包含一个许可证文件。切换到包含 go.mod 的目录,然后创建 repo:

$ git init
$ git add LICENSE go.mod go.sum hello.go hello_test.go
$ git commit -m "hello: initial commit"
$

语义版本和模块(Semantic versions and modules)

go.mod 中的每个必需模块都有一个语义版本,该依赖关系的最小版本用于构建该模块。语义版本的形式为 vMAJOR.MINOR.PATCH

  • 当您对模块的公共 API 进行向后不兼容的更改时,增加主版本。只有在绝对必要时才应这样做。
  • 在对 API 进行向后兼容的更改时,增加次要版本,例如更改依赖项或添加新函数、方法、结构字段或类型。
  • 在不影响模块的公共 API 或依赖项(如修复bug)的小更改之后,增加补丁版本。

您可以通过附加连字符和点分隔的标识符来指定预发布版本(如 v1.0.1-alpha 或 v2.2.2-beta.2)。普通版本比预发布版本更受 go 命令的青睐,但是如果您的模块有任何普通版本,用户要想获得您的预发布版本必须明确指定版本(如 gogetexample.com/hello@v1.0.1-alpha)。

v0 主要版本和预发布版本不能保证向后兼容。它们让你在向用户做出稳定性承诺之前改进你的 API。但是,v1 主要版本和更高版本需要在该主要版本中向后兼容。

go.mod 引用的版本可以是存储库中标记的显式版本(例如 v1.5.2),也可以是基于特定提交的伪版本(例如 v0.0.0-20170915032832-14c0d48ead0c)。伪版本是预发布版本的一种特殊类型。当用户需要依赖于尚未发布任何语义版本标记的项目,或者需要针对尚未标记的提交进行开发时,伪版本非常有用,但用户不应假定伪版本提供了稳定或经过良好测试的 API。最好的做法是用显式版本标记模块向用户表明特定版本已经过充分测试并可以使用。

一旦你开始用版本标记你的 repo,在你开发你的模块的时候保持对新版本进行标记是很重要的。当用户请求模块的新版本时(使用 go get-u 或 go getexample.com/hello),go 命令将选择可用的最大语义发布版本,即使该版本已存在数年,并且在主分支之后有许多更改。所以继续标记新版本将使您的持续改进对您的用户可用。

不要从 repo 中删除版本标记。如果发现某个版本存在 bug 或安全问题,请发布新版本。如果用户依赖于已删除的版本,则其编译可能会失败。同样,一旦发布了一个版本,就不要更改或覆盖它。模块镜像和校验和数据库会存储模块、它们的版本和用于校验的签名哈希值,以确保给定版本的构建随着时间的推移保持可复制性。

v0:最初的不稳定版本(v0: the initial, unstable version)

让我们用 v0 语义版本标记模块。v0 版本不提供任何稳定性保证,因此几乎所有项目都应该从 v0 开始,因为最开始需要改进项目的公共 API。

标记一个新版本有几个步骤:

1.运行 go mod tidy 命令,这将消除模块可能积累的不再需要的任何依赖项
2.跑去单元测试 go test ./..,以确保一切正常
3. 使用 git tag 命令标记用新的项目版本
4. 将新标记推送到远端仓库

示例命令如下:

$ go mod tidy
$ go test ./...
ok      example.com/hello       0.015s
$ git add go.mod go.sum hello.go hello_test.go
$ git commit -m "hello: changes for v0.1.0"
$ git tag v0.1.0
$ git push origin v0.1.0
$

现在,其他项目可以依赖于 example.com/hello的 v0.1.0。对于您自己的模块,您可以运行 go list -m example.com/hello@v0.1.0 来确认最新版本是否可用。如果您没有立即看到最新版本,并且正在使用Go 模块代理(默认为Go 1.13),请在几分钟后再试一次,给代理加载新版本一点时间。

如果添加了新的公共 API 到模块中,对 v0 模块进行了中断更改,或者升级您的依赖项的次要版本,请为你的模块的下一个版本增加次要版本。例如,v0.1.0 之后的下一个次要版本将是 v0.2.0。

如果修复了现有版本中的错误,则增加补丁版本。例如,v0.1.0 之后的下一个补丁版本将是 v0.1.1。

v1:第一个稳定版本(v1: the first stable version)

一旦您完全确定您的模块的 API 是稳定的,您就可以发布 v1.0.0。v1 的主要版本告诉用户,不会对模块的 API 进行不兼容的更改。它们可以升级到 v1 新的次要版本和补丁版本,它们的代码不会出现不兼容的错误。函数和方法签名不会更改,导出的类型不会被移除,等等。如果对 API 进行了更改,则它们将向后兼容(例如,向 struct 添加一个新字段),并将其包含在新的次要版本中。如果有错误修复(例如,安全修复),它们将包含在补丁版本中(或者作为次要版本的一部分)。

有时,保持向后兼容性可能会导致糟糕的 API。没关系。不完美的 API 比破坏用户的现有代码要好。

标准库的 strings 包是以 API 一致性为代价来保持向后兼容性的一个主要示例。

  • Split 将字符串分割成由分隔符分隔的所有子字符串,并将所有子字符串作为切片返回
  • SplitN 可以用来控制要返回的子字符串的数量

但是,Replace 从一开始就计算了要替换的字符串的实例数(不像 Split)。

给定 Split 和 SplitN,您会期望有 Replace 和 ReplaceN 这样的函数。但是,我们无法在不中断使用者的情况下更改现有的 Replace 函数,因为这是我们承诺不能做的。因此,在 Go 1.12 中,我们添加了一个新函数 ReplaceAll 而不是 ReplaceN,虽然这样的 API 名称有点奇怪,但这种不一致性比破坏的更改要好。

假设您对 example.com/hello 的 API 很满意,并且希望将 v1 作为第一个稳定版本发布。标记 v1 与标记 v0 版本过程相同:

$ go mod tidy
$ go test ./...
ok      example.com/hello       0.015s
$ git add go.mod go.sum hello.go hello_test.go
$ git commit -m "hello: changes for v1.0.0"
$ git tag v1.0.0
$ git push origin v1.0.0
$

此时,example.com/hello 的 v1 版本的 API 就固化了,这向每个人传达了我们的 API 是稳定的,他们应该觉得使用它很舒服。

结论(conclusion)

这篇文章讲述了使用语义版本标记模块以及何时发布 v1 的过程。未来的一篇文章将讨论如何维护和发布 v2 及更高版本的模块。

为了帮助在 Go 中塑造依赖管理的未来,请发送错误报告经验报告

感谢您的反馈。

相关阅读(Related articles)

 类似资料: