:::info 日期:2019 年 09 月 26 日
作者:Tyler Bui-Palsulich
原文链接:https://go.dev/blog/publishing-go-modules :::

介绍

这篇文章是系列文章的第 3 部分

注意:有关开发模块的文档,请参阅开发和发布模块

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

请注意:这篇文章涵盖了直到并包括 v1.0 的开发。 如果您对 v2 感兴趣,请参阅 Go Modules: v2 and Beyond

这篇文章在示例中使用了 GitMercurialBazaar 和其他公司也受支持。

创建项目

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

  1. $ cat go.mod
  2. module example.com/hello
  3. go 1.12
  4. require rsc.io/quote/v3 v3.1.0
  5. $ cat go.sum
  6. golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
  7. golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
  8. rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
  9. rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
  10. rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
  11. rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
  12. $ cat hello.go
  13. package hello
  14. import "rsc.io/quote/v3"
  15. func Hello() string {
  16. return quote.HelloV3()
  17. }
  18. func Proverb() string {
  19. return quote.Concurrency()
  20. }
  21. $ cat hello_test.go
  22. package hello
  23. import (
  24. "testing"
  25. )
  26. func TestHello(t *testing.T) {
  27. want := "Hello, world."
  28. if got := Hello(); got != want {
  29. t.Errorf("Hello() = %q, want %q", got, want)
  30. }
  31. }
  32. func TestProverb(t *testing.T) {
  33. want := "Concurrency is not parallelism."
  34. if got := Proverb(); got != want {
  35. t.Errorf("Proverb() = %q, want %q", got, want)
  36. }
  37. }
  38. $

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

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

语义版本和模块

go.mod 中的每个必需模块都有一个语义版本,即用于构建模块的该依赖项的最低版本。

语义版本的形式为 vMAJOR.MINOR.PATCH。

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

您可以通过附加连字符和点分隔的标识符(例如,v1.0.1-alpha 或 v2.2.2-beta.2)来指定预发布版本。 go 命令比预发布版本更喜欢普通版本,因此如果您的模块有任何普通版本,用户必须明确要求预发布版本(例如,go get example.com/hello@v1.0.1-alpha)。

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

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

一旦您开始使用版本标记您的存储库,在开发模块时继续标记新版本非常重要。 当用户请求模块的新版本时(使用 go get -u 或 go get example.com/hello),go 命令将选择可用的最大语义发布版本,即使该版本已有数年历史并且后面有许多更改 初级分支。 继续标记新版本将使您的用户可以使用正在进行的改进。

不要从你的仓库中删除版本标签。 如果您发现某个版本存在错误或安全问题,请发布新版本。 如果人们依赖于您已删除的版本,则他们的构建可能会失败。 同样,一旦发布版本,请勿更改或覆盖它。 模块镜像和校验和数据库存储模块、它们的版本和签名的加密哈希,以确保给定版本的构建随着时间的推移保持可重现。

v0:最初的、不稳定的版本

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

标记新版本有几个步骤:

  1. 运行 go mod tidy,这会删除模块可能积累的不再需要的任何依赖项。
  2. 运行 go test ./… 最后一次以确保一切正常。
  3. 使用 git tag 用新版本标记项目。
  4. 将新标签推送到源存储库。
    1. $ go mod tidy
    2. $ go test ./...
    3. ok example.com/hello 0.015s
    4. $ git add go.mod go.sum hello.go hello_test.go
    5. $ git commit -m "hello: changes for v0.1.0"
    6. $ git tag v0.1.0
    7. $ git push origin v0.1.0
    8. $
    现在其他项目可以依赖example.com/hello的v0.1.0。 对于您自己的模块,您可以运行 go list -m example.com/hello@v0.1.0 以确认最新版本可用(此示例模块不存在,因此没有可用版本)。 如果您没有立即看到最新版本并且您正在使用 Go 模块代理(自 Go 1.13 以来的默认设置),请在几分钟后重试,让代理有时间加载新版本。

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

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

v1:第一个稳定版本

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

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

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

  • Split 将字符串切片为由分隔符分隔的所有子字符串,并返回这些分隔符之间的子字符串的切片。
  • SplitN 可用于控制要返回的子字符串的数量。

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

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

假设您对 example.com/hello 的 API 感到满意,并且想要发布 v1 作为第一个稳定版本。

标记 v1 使用与标记 v0 版本相同的过程:运行 go mod tidy 和 go test ./…,标记版本,并将标记推送到源存储库:

  1. $ go mod tidy
  2. $ go test ./...
  3. ok example.com/hello 0.015s
  4. $ git add go.mod go.sum hello.go hello_test.go
  5. $ git commit -m "hello: changes for v1.0.0"
  6. $ git tag v1.0.0
  7. $ git push origin v1.0.0
  8. $

至此,example.com/hello的v1 API就固化了。 这向每个人传达我们的 API 是稳定的,他们应该可以放心使用它。

总结

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

为了提供反馈并帮助塑造 Go 依赖管理的未来,请向我们发送错误报告体验报告

感谢您的所有反馈并帮助改进 Go 模块。