Golang依赖包管理知多少(3)-发布一个module

前言

这篇文章将讨论如何编写并发布可被其他module依赖引用的一个module。

注意:这篇主要涉及开发配置直至v1版本,如果你对v2版本迭代感兴趣,可以参考v2版本与版本迭代

本文默认使用Git作为例子, 但是Mercurial、Bazaar等其他版本管理工具也是支持的。

项目配置

本文需要你有一个已经存在的项目作为例子,所以我们以认识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仓库并且初始化一个提交。如果你要公开发布你的项目请确保包含LICENSE文件,当然目录中也要包含go.mod文件,最后创建仓库:

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

版本号与module

go.mod文件每个被依赖的module都有明确的版本号,标识着在构建当前module时所需要的最小版本依赖。

版本号的格式如下vMAJOR.MINOR.PATCH

  • MAJOR增加,意味着module中的公开API可能有无法向后兼容的变更
  • MINOR增加,意味着module中的API有更改但可以向后兼容,比如更改依赖关系或者增加一个新的函数、方法、结构变量、类型等等。
  • PATCH增加,意味着不会变更API和依赖,只是对MINOR版本做出小的变更,比如修复一个bug。

当然你也可以发布一个预发布版本,在版本号的后面紧跟一个连字符和点号标记(例如v1.0.1-alpha、v2.2.2-beta.2)。go命令优先使用正式版本而不是预发布版本,所以一个module有正式版本的情况下,用户如果想使用一个预发布版本就需要明确指定,比如go get exmaple.com/hello@v1.0.1-alpha

v0 版本和预发布版本不保证向后兼容,这让你可以在代码稳定提供给用户之前优化你的API。但是v1以及之后的大版本必须在各自的MINOR版本内保持向后兼容性。

一个module的版本会显性地在go.mod文件中标明比如v1.5.2,或者使用基于commitId的伪版本号比如v0.0.0-20170915032832-14c0d48ead0c。伪版本是一类特殊的预发布版本,伪版本在用户使用了一个还未公开发布也没有标明版本tag的moudle时是非常有用的,但用户不能假设这个module的API都是稳定测试过的。显示标明一个明确的版本意味着这些版本是对外公开测试通过且可用的API。

一旦开始用版本号标明你的仓库,随着开发迭代module保持标记tag是非常重要的。用户不管使用go get -u还是go get exmaple.com/hello命令来请求一个module新版本的时候,go都会选择最大版本号,不管他们是几年前的版本还是在主分支之后做了哪些变更。不断的标记最新release版本对你的用户来说是一个持续提升。

不要从你的仓库中删除一个版本tag。如果你在一个版本上发现了bug或者安全问题,请发布一个新版本。如果用户依赖了一个你已经删除的版本,那他们将构建失败。简单说来,一旦你对外发布了一个版本,请不要修改或者删除这个版本。module镜像签名数据库已经存储了这个module、版本号、HASH签名来保证随着时间的推移给定的版本依然可用。

v0-初始版本、非稳定版本

让我们从标记v0版本开始。v0版本不承担API的稳定性,这就好比许多项目都是从v0版本开始逐渐优化它们的公开API一样。

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

  1. 运行go mod tidy命令,它会移除module中累积但没有必要的依赖项。
  2. 最后运行go test ./..命令,确保所有代码一切运行正常。
  3. 使用git tag标记项目到一个新的版本号。
  4. 发布新的tag版本号到远端仓库。
$ 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版本了。如果是你自己的module,你就可以执行go list -m example.com/hello@v0.1.0来确认最新版本可用(当然这个例子本身就不存在,所以也就是不可用了)。如果你没有马上看到最新版本或者你正在使用Go module代理模式(从Go1.13开始默认启用),请稍等几分钟以便于代理服务拉取到最新版本再试一下。

如果你在v0版本的module中增加了公开API、对代码做了重大的变更、升级了当前module的依赖module的MINOR版本号、请增加当前module的minor版本号。比如v0.1.0的下一个版本就是v0.2.0

如果你修复一个已经发布的版本的bug,请增加PATCH版本号.比如v0.1.0的下一个版本就是v0.1.1

v1-第一个稳定版本

一旦你觉得你module中的公开API没有问题了,那就可以发布v1版本了。v1大版本意味着对用户来说不会有对module中API任何不兼容的变更。用户可以升级到新的v1版本或者是它的patch版本而不会导致编译执行问题,比如函数和方法签名不会再改变,导出类型不会被移除等等。如果有任意的API变更,它们也会保持向后兼容性(比如在结构体内增加一个新的变量)并发布一个MINOR版本。如果有bug修复(比如安全bug)会发布一个PATCH版本。

有时为了保持向后兼容性可能导致API混乱。没关系,不完善的API总比让用户已有的代码崩溃好。

标准库中的string包就是一个以API一致性为代价而维持向后兼容性的例子。

  • Split函数用于将一个字符串按照分隔符切分为子字符串并返回。
  • SplitN函数用于控制切分子字符串的个数的返回

然而,Replace函数不像Split那样,这个函数从开始设计时就需要指定要替换的字符串实例的个数。

从给出的SplitSpitN函数看,你可能对应到ReplaceReplaceN,但是如果不对调用Replace函数的使用方做更改那我们可能也无法做出函数的更改,当然我们承诺也不会那样做。所以在Go1.12,我们增加一个新的函数ReplaceAll。虽然这样的结果有点尴尬,SplitReplace的使用方法竟然是不同,但非一致性的代价好过破坏性的变更。

如果你对example.com/hello的API感觉稳定了想要发布的v1版本,使用和v0发布时一样的步骤,执行go mod tidygo test ./..,标记版本号、推送tag到远程仓库:

$ 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的用户调用都是稳定的,他们可以高兴舒服地使用这些API了。