跳转到主要内容

介绍


许多现代编程语言允许开发人员分发现成的库供其他人在他们的程序中使用,Go 也不例外。虽然某些语言使用中央存储库来安装这些库,但 Go 从用于创建库的同一版本控制存储库分发它们。 Go 还使用称为语义版本控制的版本控制系统向用户显示何时以及进行了哪些更改。这有助于用户了解模块的较新版本是否可以安全地快速升级,并有助于确保他们的软件继续与模块一起工作。

在本教程中,您将创建和发布新模块,学习使用语义版本控制,并发布模块的语义版本。

先决条件

 

  • 安装 1.16 或更高版本。要进行设置,请按照您的操作系统的如何安装 Go 教程进行操作。
  • 对 Go 模块的理解,您可以在如何使用 Go 模块教程中找到。
  • 熟悉 Git,您可以通过遵循如何使用 Git:参考指南获得。
  • 一个名为 pubmodule 的空公共 GitHub 存储库,用于您的已发布模块。要开始使用,请按照 GitHub 文档创建存储库。


创建要发布的模块


与许多其他编程语言不同,Go 模块直接从它所在的源代码存储库分发,而不是独立的包存储库。这使用户更容易找到在他们的代码中引用的模块,并且模块维护者可以更容易地发布他们模块的新版本。在本节中,您将创建一个新模块,然后将其发布以供其他用户使用。

要开始创建模块,您将在您创建的空存储库上使用 git clone 作为先决条件的一部分,以下载初始存储库。该存储库可以克隆到您计算机上任何您想要的位置,但许多开发人员倾向于为他们的项目创建一个目录。在本教程中,您将使用一个名为 projects 的目录。

创建项目目录并导航到它:

mkdir projects
cd projects


在项目目录中,运行 git clone 将您的存储库克隆到您的计算机:

git clone git@github.com:your_github_username/pubmodule.git


克隆模块会将您的空模块下载到项目目录中的 pubmodule 目录中。你可能会收到一个警告,说你克隆了一个空的存储库,但这没什么好担心的:

Output
Cloning into 'pubmodule'...
warning: You appear to have cloned an empty repository.


接下来,切换到您下载的目录:

cd pubmodule


进入模块目录后,您将使用 go mod init 创建新模块并将存储库的位置作为模块名称传递。确保模块名称与存储库位置匹配很重要,因为这是 go 工具在其他项目中使用模块时查找模块下载位置的方式:

go mod init github.com/your_github_username/pubmodule


Go 将通过让您知道它已创建 go.mod 文件来确认您的模块已创建:

Output 

go: creating new go.mod: module github.com/your_github_username/pubmodule


最后,使用您最喜欢的文本编辑器,例如 nano,创建并打开一个与您的存储库同名的文件:pubmodule.go。

nano pubmodule.go


此文件的名称可以是任何名称,但使用与包相同的名称可以更容易地知道在使用不熟悉的包时从哪里开始。但是,包名称本身应该与您的存储库名称相同。这样,当有人从您的包中引用方法或类型时,它会匹配存储库,例如 pubmodule.MyFunction。这将使他们更容易知道包裹来自哪里,以防他们以后需要参考它。

接下来,向您的包中添加一个 Hello 方法,该方法将返回字符串 Hello, You!。这将是任何导入您的包的人都可以使用的功能:

projects/pubmodule/pubmodule.go

package pubmodule

func Hello() string {
  return "Hello, You!"
}

您现在已经使用 go mod init 创建了一个新模块,其模块名称与您的远程存储库 (github.com/your_github_username/pubmodule) 匹配。您还向您的模块添加了一个名为 pubmodule.go 的文件,其中包含一个名为 Hello 的函数,您的模块的用户可以调用该函数。接下来,您将发布您的模块以供其他人使用。

发布模块


一旦您创建了一个本地模块并准备好将其提供给其他用户,就该发布您的模块了。由于 Go 模块是从存储它们的相同代码存储库分发的,因此您将代码提交到本地 Git 存储库,并将其推送到位于 github.com/your_github_username/pubmodule 的存储库。

在您将代码提交到本地 Git 存储库之前,最好确保您不会提交任何您不希望提交的文件,然后在您将代码推送到 GitHub 时这些文件将公开发布。在 pubmodule 目录中使用 git status 命令将显示所有将提交的文件和更改:

git status


输出将类似于以下内容:

Output
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    go.mod
    pubmodule.go

您应该会看到 go mod init 命令创建的 go.mod 文件,以及您在其中创建 Hello 函数的 pubmodule.go 文件。根据您创建存储库的方式,您可能具有与此输出不同的分支名称。最常见的是,名称将是 main 或 master。

当您确定只有您要查找的文件时,您可以使用 git add 暂存文件并使用 git commit 将它们提交到存储库:

git add .
git commit -m "Initial Commit"


输出将类似于以下内容:

Output
[main (root-commit) 931071d] Initial Commit
 2 files changed, 8 insertions(+)
 create mode 100644 go.mod
 create mode 100644 pubmodule.go


最后,使用 git push 命令将您的模块推送到 GitHub 存储库:

git push


输出将类似于以下内容:

Output
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 367 bytes | 367.00 KiB/s, done.
Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:your_github_username/pubmodule.git
 * [new branch]      main -> main


运行 git push 命令后,您的模块将被推送到您的存储库,现在可供其他任何人使用。如果您没有发布任何版本,Go 将使用存储库默认分支中的代码作为模块的代码。如果您的默认分支命名为 main、master 或其他名称,则无关紧要,只要您的存储库的默认分支设置为什么即可。

在本节中,您将创建的本地 Go 模块发布到您的 GitHub 存储库,以供其他人使用。虽然您现在有一个已发布的模块,但维护公共模块的另一部分是确保您的模块的用户可以使用它的稳定版本。您可能希望在以后的模块中进行更改并添加功能,但是如果您在不使用模块中的版本的情况下进行这些更改,您可能会意外破坏使用您模块的人的代码。为了解决这个问题,您可以在开发中达到新的里程碑时向您的模块添加版本。但是,在添加新版本时,请务必选择一个有意义的版本号,以便您的用户知道他们立即升级是否安全。

语义版本控制


一个有意义的版本号可以让您的用户了解他们与之交互的公共接口或 API 发生了多少变化。 Go 通过称为语义版本控制或简称为“SemVer”的版本控制方案来传达这些更改。 (语义版本控制使用版本字符串来传达有关代码更改的含义,这就是语义版本控制得名的地方。)Go 的模块系统遵循 SemVer 来确定哪些版本比您当前使用的版本更新,以及是否更新模块的版本可以安全地自动升级。

语义版本控制为版本字符串中的每个数字赋予了含义。 SemVer 中的典型版本包含三个主要数字:主要版本、次要版本和补丁版本。这些数字中的每一个都与 .形成版本,如1.2.3。编号的顺序是主要版本在前,次要版本在后,补丁版本在后。这样,在查看版本时,您可以看到哪个版本更新,因为特定位置的数字高于以前的版本。例如,版本 2.2.3 比 1.2.3 更新,因为主要版本更高。同样,版本 1.4.3 比 1.2.10 更新,因为次要版本更高。尽管补丁版本中 10 高于 3,但次要版本 4 高于 2,因此该版本优先。当版本字符串中的数字增加时,其后版本的所有其他部分都将重置为 0。例如,增加 1.3.10 的次要版本将导致 1.4.0,增加 2.4.1 的主要版本将导致在 3.0.0 中。

使用这些规则允许 Go 在运行 go get 时确定要使用的模块版本。例如,假设您有一个使用 1.4.3 版本的模块 github.com/your_github_username/pubmodule 的项目。如果您依赖 pubmodule 稳定,您可能只想自动升级补丁版本(.3)。如果您运行命令 go get -u=patch github.com/your_github_username/pubmodule,Go 会看到您想要升级模块的补丁版本,并且只会查找以 1.4 作为主要和次要部分的新版本版本。

在创建模块的新版本时,重要的是要考虑模块的公共 API 发生了怎样的变化。语义版本字符串的每个部分都向您和您的用户传达了 API 更改的范围。这些类型的更改通常分为三个不同的类别,与版本的每个组件一致。最小的更改会增加补丁版本,中等的更改会增加次要版本,最大的更改会增加主要版本。使用这些类别来确定要增加的版本号将帮助您避免破坏自己的代码以及依赖您模块的任何其他人的代码。

主要版本号


SemVer 版本中的第一个数字是主要版本号 (1.4.3)。主版本号是发布模块新版本时要考虑的最重要的数字。主要版本更改用于向您的公共 API 发出向后不兼容的更改信号。向后不兼容的更改将是您的模块中的任何更改,如果他们升级而不进行任何其他更改,则会导致某人的程序中断。中断可能意味着由于函数名称已更改而无法构建,或者库工作方式的更改导致相同的方法返回“v1”而不是“1”。不过,这仅适用于您的公共 API,这意味着其他人可以使用的任何导出类型或方法。如果该版本仅包含您库的用户不会注意到的改进,则不需要进行主要版本更改。记住哪些更改适合此类别的一种方法可能是任何被视为“更新”或“删除”的内容都将是主要版本的增加。

注意:与 SemVer 中其他类型的数字不同,主版本 0 具有额外的特殊意义。主要版本 0 被认为是“开发中”版本。任何主版本为 0 的 SemVer 都被认为是不稳定的,API 中的任何内容都可能随时更改。当您创建一个新模块时,最好从主要版本 0 开始,并且只更新次要版本和补丁版本,直到您完成模块的初始开发。一旦您的模块的公共 API 完成更改并被认为对您的用户来说是稳定的,就该从 1.0.0 版本开始了。

以以下代码为例,说明主要版本更改可能是什么样子。您有一个名为 UserAddress 的函数,它当前接受一个字符串作为参数并返回一个字符串:

func UserAddress(username string) string {
    // return user address as a string
}


虽然该函数当前返回一个字符串,但您可能会确定,如果该函数返回一个像 *Address 这样的结构,对您和您的用户来说会更好。通过这种方式,您可以包含已拆分的其他数据,例如邮政编码:

type Address struct {
    Address    string
    PostalCode string
}

func UserAddress(username string) *Address {
    // return user address and postal code struct
}

这将是主要版本更改的示例,因为它需要您的用户更改他们自己的代码才能使用它。如果您决定完全删除 UserAddress 也是如此,因为您的用户需要更新他们的代码才能使用替换。

另一个主要版本更改的示例是向 UserAddress 函数添加一个新参数,即使它仍然返回一个字符串:

func UserAddress(username string, uppercase bool) string {
    // return user address as a string, uppercase if bool is true
}


由于此更改还要求您的用户在使用 UserAddress 函数时更新他们的代码,因此还需要增加主要版本。

不过,并非您对代码所做的所有更改都会如此剧烈。有时您会更改公共 API 以添加新功能或值,但不会更改任何现有功能或值。

次要版本号


SemVer 版本中的第二个数字是次要版本号 (1.4.3)。次要版本更改用于向您的公共 API 发出向后兼容的更改信号。向后兼容的更改是任何不影响当前使用您的模块的代码或项目的更改。与主要版本号类似,这只影响您的公共 API。记住哪些更改适合此类别的一种方法可能被视为“添加”,而不是“更新”。

使用主要版本号中的相同示例,假设您有一个名为 UserAddress 的方法,它返回一个字符串:

func UserAddress(username string) string {
    // return user address as a string
}

不过,这一次,您决定添加一个名为 UserAddressDetail 的全新方法,而不是更新 UserAddress 以返回 *Address:

type Address struct {
    Address    string
    PostalCode string
}

func UserAddress(username string) string {
    // return user address as a string
}

func UserAddressDetail(username string) *Address {
    // return user address and postal code struct
}

添加这个新的 UserAddressDetail 函数不需要用户更改,如果他们更新到你的模块的这个版本,所以它会被认为是一个次要的版本号增加。他们可以继续使用 UserAddress,并且只需要在他们希望包含来自 UserAddressDetail 的附加信息时更新他们的代码。

不过,公共 API 更改可能不是您发布模块新版本的唯一时间。错误是软件开发中不可避免的一部分,补丁版本号就是为了掩盖这些漏洞。

补丁版本号


补丁版本号是 SemVer 版本 (1.4.3) 中的最后一个数字。补丁版本更改是不影响模块公共 API 的任何更改。不影响模块公共 API 的更改往往是错误修复或安全修复。再次使用前面示例中的 UserAddress 函数,假设您的模块版本在函数返回的字符串中缺少地址的一部分。如果您发布模块的新版本来修复该错误,则只会增加补丁版本。该版本不包括对用户如何使用 UserAddress 公共 API 的任何更改,仅包括返回数据的正确性。

正如您在本节中所见,仔细选择新版本号是赢得用户信任的重要方式。使用语义版本控制向用户展示了更新到新版本所需的工作量,并且您不会意外地因更新破坏他们的程序而让他们感到惊讶。在考虑您对模块所做的更改并确定要使用的下一个版本号之后,您可以发布新版本并使其可供用户使用。

发布新的模块版本


在发布模块的新版本之前,您需要使用计划进行的更改来更新模块。如果不进行任何更改,您将无法确定要增加语义版本的哪一部分。对于本教程中的模块,您将添加一个新的 Goodbye 方法来补充 Hello 方法,然后您将发布该新版本供用户使用。

首先,打开 pubmodule.go 文件并将新的 Goodbye 方法添加到您的公共 API:

pubmodule/pubmodule.go

package pubmodule

func Hello() string {
  return "Hello, You!"
}

func Goodbye() string {
  return "Goodbye for now!"
}

保存更改后,您需要通过运行 git status 来检查预期要提交的更改:

git status


输出将与此类似,表明您的模块中唯一的变化是您添加到 pubmodule.go 的方法:

Output
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   pubmodule.go

no changes added to commit (use "git add" and/or "git commit -a")


接下来,将更改添加到暂存文件并使用 git add 和 git commit 将更改提交到本地存储库:

git add .
git commit -m "Add Goodbye method"


输出将类似于以下内容:

Output
[main 3235010] Add Goodbye method
 1 file changed, 4 insertions(+)


提交更改后,您需要将它们推送到您的 GitHub 存储库。在较大的软件项目中,或者在与其他开发人员一起工作时,此步骤通常会略有不同。在开发新功能时,开发人员会创建一个 Git 分支来进行更改,直到新功能稳定并准备好发布。一旦发生这种情况,另一个开发人员将审查分支中的更改,以添加第二双眼睛,可能会发现第一个开发人员可能错过的问题。审核完成后,该分支将被合并到默认分支中(例如 master 或 main)。在发布之间,默认分支会累积这些类型的更改,直到发布新版本为止。

由于您的模块在这里没有经过此过程,因此将您所做的更改推送到存储库将模拟更改的累积:

git push


输出将类似于以下内容:

Output
numerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 369 bytes | 369.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:your_github_username/pubmodule.git
   931071d..3235010  main -> main


输出显示新代码已准备好供用户在默认分支中使用。

到目前为止,您所做的一切都与最初发布您的模块相同。然而,现在发布新版本的一个重要部分出现了:选择一个新的版本号。

如果您查看您对模块所做的更改,那么对公共 API 的唯一更改(或实际上任何更改)是将 Goodbye 方法添加到您的模块中。由于用户可以从仅具有 Hello 功能的先前版本进行更新,而无需对其进行更改,因此此更改将是向后兼容的更改。在语义版本控制中,对公共 API 的向后兼容更改将意味着次要版本号的增加。不过,这是您发布的模块的第一个版本,因此没有以前的版本需要增加。如果您认为 0.0.0 是“无版本”,那么增加次要版本将导致您进入 0.1.0 版本,即模块的下一个版本。

既然您已经为模块的发布提供了版本号,您可以使用它与 Git 标记配对来发布新版本。当开发人员使用 Git 来跟踪他们的源代码时,即使使用 Go 以外的语言,一个常见的约定是使用 Git 的标签来跟踪为特定版本发布了哪些代码。这样,如果他们需要对旧版本进行更改,他们可以使用该标签。由于 Go 已经从源存储库下载模块,它通过使用相同的版本标签来利用这种做法。

要使用这些标签发布您自己的模块的新版本,您将使用 git tag 命令标记您正在发布的代码。作为 git tag 命令的参数,您还需要提供版本标签。要创建版本标记,请以前缀 v 开头,表示版本,并在其后立即添加您的 SemVer。对于您的模块,您的最终版本标记将是 v0.1.0。现在,运行 git tag 以使用版本标签标记您的模块:

git tag v0.1.0


在本地添加版本标签后,您仍然需要将标签推送到您的 GitHub 存储库,您可以使用带有 origin 的 git push 来执行此操作:

git push origin v0.1.0


git push 命令成功后,您会看到一个新标签 v0.1.0 已创建:

Output
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To github.com:your_github_username/pubmodule.git
 * [new tag]         v0.1.0 -> v0.1.0


上面的输出显示您的标签已被推送,并且您的 GitHub 存储库有一个新的 v0.1.0 标签可供您模块的用户参考。

现在您已经使用 git 标签发布了模块的新版本,每当用户运行 go get 获取模块的最新版本时,它将不再从默认分支下载基于最新提交哈希的版本。一旦模块发布了版本,go 工具将开始使用这些版本来确定更新模块的最佳方式。与语义版本控制相结合,这允许您迭代和改进您的模块,同时还为您的用户提供一致和稳定的体验。

结论


在本教程中,您创建了一个公共 Go 模块并将其发布到 GitHub 存储库,以便其他人可以使用它。您还使用语义版本控制来确定模块的最佳版本号。最后,您扩展了模块的功能,并使用语义版本控制发布了新版本,并确信您不会破坏依赖它的程序。

如果您想了解有关语义版本控制的更多信息,包括如何将数字以外的信息添加到您的版本中,语义版本控制网站将提供非常详细的信息。 Go 文档还有一个模块版本编号页面,解释了 Go 具体如何使用 SemVer。

有关 Go 模块的更多信息,Go 项目有一系列博客文章,详细介绍了 Go 工具如何与模块交互和理解模块。 Go 项目在 Go Modules Reference 中也有关于 Go 模块的非常详细和技术性的参考。

文章链接