Go 官方博客

发布 Go 模块

Tyler Bui-Palsulich
2019 年 9 月 26 日

引言

本文是系列文章的第三部分。

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

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

请注意:本文涵盖了直至并包括 v1 的开发。如果您对 v2 感兴趣,请参阅Go 模块:v2 及后续版本

本文示例中使用 GitMercurialBazaar 等也受支持。

项目设置

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

$ 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 仓库并添加初始提交。如果您正在发布自己的项目,请确保包含 LICENSE 文件。切换到包含 go.mod 的目录,然后创建仓库。

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

语义版本与模块

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

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

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

您可以通过附加连字符和点分隔的标识符来指定预发布版本(例如 v1.0.1-alphav2.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 -ugo get example.com/hello),go 命令将选择可用的最高语义发布版本,即使该版本已存在多年且落后于主分支许多更改。继续标记新版本将使您的持续改进可供用户使用。

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

v0:初始的不稳定版本

让我们为模块标记一个 v0 语义版本。v0 版本不提供任何稳定性保证,因此几乎所有项目在完善其公共 API 时都应从 v0 开始。

标记新版本有几个步骤

  1. 运行 go mod tidy,这将移除模块可能积累的不再需要的依赖项。

  2. 最后一次运行 go test ./... 以确保一切正常。

  3. 使用 git tag 为项目标记一个新版本。

  4. 将新标签推送到 origin 仓库。

$ 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/hellov0.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.0v1 主要版本向用户表明不会对模块的 API 进行不兼容的更改。他们可以升级到新的 v1 次要版本和补丁版本,并且他们的代码不应该中断。函数和方法的签名不会改变,导出的类型不会被移除等等。如果 API 有变化,它们将是向后兼容的(例如,向结构体添加一个新字段),并将包含在新发布的次要版本中。如果存在错误修复(例如,安全修复),它们将包含在补丁版本中(或作为次要版本的一部分)。

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

标准库的 strings 包是为保持向后兼容性而牺牲 API 一致性的一个典型例子。

  • Split 将字符串按分隔符分割成所有子字符串,并返回这些分隔符之间的子字符串切片。
  • SplitN 可以用来控制返回的子字符串数量。

然而,Replace 需要一个计数来指定从开头替换字符串的多少个实例(与 Split 不同)。

考虑到 SplitSplitN,您可能会期望有类似 ReplaceReplaceN 的函数。但是,我们不能在不破坏调用者代码的情况下更改现有的 Replace,而我们承诺过不这样做。因此,在 Go 1.12 中,我们添加了一个新函数 ReplaceAll。最终的 API 有点奇怪,因为 SplitReplace 的行为不同,但这种不一致比破坏性更改要好。

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

标记 v1 的过程与标记 v0 版本相同:运行 go mod tidygo test ./...,标记版本,然后将标签推送到 origin 仓库。

$ 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/hellov1 API 已确定。这向所有人表明我们的 API 是稳定的,他们应该可以放心使用。

结论

本文详细介绍了使用语义版本标记模块以及何时发布 v1 的过程。未来的文章将介绍如何在 v2 及后续版本中维护和发布模块。

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

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

下一篇文章:处理 Go 1.13 中的错误
上一篇文章:Go 1.13 发布
博客索引