如何发布自己 go module 到 Google 的包管理平台
9 min read

如何发布自己 go module 到 Google 的包管理平台

手把手教学如何在 Google 的 go 官方包管理平台 pkg.go.dev 上发布自己包,方便其他开发者可以轻松导入和使用自己的代码,另外深入讲解了基于代码注释的 godoc 生态是如何管理 pkg.go.dev 上的文档。
如何发布自己 go module 到 Google 的包管理平台
Photo generated by Gemini

对于使用 go 语言的开发者,当你想让自己开发的一个模块包可共享给全世界其他开发人员使用时,可以选择在 go 官方包管理平台发布它,这样 go 的生态工具可以看到它,发布模块后,导入其包的开发人员将能够通过运行 go get 等命令来解决对该模块的依赖关系。

如下图所示,我自己在 Google 的包管理平台发布的一个 go module

P.S.:已经发布模块需要修改时不要更改当前发布版本的代码而是发布新版本,因为 go 工具会根据第一个下载的副本对下载的模块进行身份验证,如果两者不同 go 工具将返回安全错误。

1. 发布步骤

  1. 在命令行中进入需要发布的包的本地根目录
  2. 运行 go mod tidy ,这会删除模块可能积累的不再需要的任何依赖项
  3. 运行 go test ./... 以确保一切正常,这会运行 go 测试框架来编译和测试编写的单元测试,一方面验证发布包的正确性,另外一方面也会编译包文档
  4. 使用 git tag ${version-num} 命令用新版本号标记项目,对于版本号可以参考下面的 go 包版本号命名表格
  5. 使用 git push origin ${version-num} 将新标签 tag 推送到远程仓库如 github.com
  6. 运行 go list 命令发布包,该命令会触发 Google 包管理平台更新对应模块索引,在命令前面添加一条语句将 GOPROXY 环境变量设置为 go 代理,从而确保发布包的请求到达官方包管理平台,GOPROXY=proxy.golang.org go list -m github.com/mymodule@${version-num}
  7. 上述步骤完成之后,任何对模块感兴趣的开发人员可以 go get 命令获取该包,然后在代码中导入该包(跟使用其他模块一样),go get 命令会获取最新版本也可以指定特定版本 go get github.com/mymodule@${version-num}

P.S.:关于 go 包版本号命名参考如下表格:

Version stage Example Message to developers
In development Automatic pseudo-version number v0.x.x Signals that the module is still in development and unstable. This release carries no backward compatibility or stability guarantees.
Major version v1.x.x Signals backward-incompatible public API changes. This release carries no guarantee that it will be backward compatible with preceding major versions.
Minor version vx.4.x Signals backward-compatible public API changes. This release guarantees backward compatibility and stability.
Patch version vx.x.1 Signals changes that don't affect the module's public API or its dependencies. This release guarantees backward compatibility and stability.
Pre-release version vx.x.x-beta.2 Signals that this is a pre-release milestone, such as an alpha or beta. This release carries no stability guarantees.

2. 基于注释的文档

完成 go list 之后在 https://pkg.go.dev/ 就可以看到发布的包了,还有一个需要解决就是如何在包代码仓库中管理 pkg.go.dev 上面文档。

godoc 生态

go 生态中采用 godoc 标准的包文档发布和共享工具,它是基于代码注释自动生成文档:

  • 提供基于 Web 的界面,可以轻松访问包文档
  • 文档直接从代码注释生成,确保它保持最新并与代码库保持同步
  • godoc 与 go 生态系统无缝集成,使其成为开发人员记录和共享包的标准工具

1. 安装 godoc

确保已经安装了 go,然后执行如下命令:

go install golang.org/x/tools/cmd/godoc@latest

2. 文档注释

文档注释(Doc Comments)是紧跟在包顶层、const、func、type 和 var 声明之前出现的注释,中间没有换行符。每个导出的(大写)名称都应该有一个文档注释。

go 文档注释的生态包括:

  • go/docgo/doc/comment 包提供了从 go 源代码中提取文档的功能
  • go doc 命令查找并打印给定包或符号(顶层的 const、func、type 或 var)的文档注释;
  • Web 服务器 pkg.go.dev 显示公共 go 包的文档;
  • pkg.go.dev 站点提供服务的程序是 golang.org/x/pkgsite/cmd/pkgsite,它也可以在本地运行以查看私有模块的文档或无需互联网连接;
  • 语言服务器 gopls 在 IDE 中编辑 go 源文件时提供文档;

以下示例说明一下 go 文档注释(具体内容参考Go Doc Comments):

  • Package
// Package path implements utility routines for manipulating slash-separated
// paths.
//
// The path package should only be used for paths separated by forward
// slashes, such as the paths in URLs. This package does not deal with
// Windows paths with drive letters or backslashes; to manipulate
// operating system paths, use the [path/filepath] package.
package path
  • Command
/*
Gofmt formats Go programs.
It uses tabs for indentation and blanks for alignment.
Alignment assumes that an editor is using a fixed-width font.

Without an explicit path, it processes the standard input. Given a file,
it operates on that file; given a directory, it operates on all .go files in
that directory, recursively. (Files starting with a period are ignored.)
By default, gofmt prints the reformatted sources to standard output.

Usage:

    gofmt [flags] [path ...]

The flags are:

    -d
        Do not print reformatted sources to standard output.
        If a file's formatting is different than gofmt's, print diffs
        to standard output.
    -w
        Do not print reformatted sources to standard output.
        If a file's formatting is different from gofmt's, overwrite it
        with gofmt's version. If an error occurred during overwriting,
        the original file is restored from an automatic backup.

When gofmt reads from standard input, it accepts either a full Go program
or a program fragment. A program fragment must be a syntactically
valid declaration list, statement list, or expression. When formatting
such a fragment, gofmt preserves leading indentation as well as leading
and trailing spaces, so that individual sections of a Go program can be
formatted by piping them through gofmt.
*/
package main
  • Types
package zip

// A Reader serves content from a ZIP archive.
type Reader struct {
    ...
}
  • Funcs
package strconv

// Quote returns a double-quoted Go string literal representing s.
// The returned string uses Go escape sequences (\t, \n, \xFF, \u0100)
// for control characters and non-printable characters as defined by IsPrint.
func Quote(s string) string {
    ...
}
  • Consts
package scanner // import "text/scanner"

// The result of Scan is one of these tokens or a Unicode character.
const (
    EOF = -(iota + 1)
    Ident
    Int
    Float
    Char
    ...
)

// Version is the Unicode edition from which the tables are derived.
const Version = "13.0.0"
  • Vars
package fs

// Generic file system errors.
// Errors returned by file systems can be tested against these errors
// using errors.Is.
var (
    ErrInvalid    = errInvalid()    // "invalid argument"
    ErrPermission = errPermission() // "permission denied"
    ErrExist      = errExist()      // "file already exists"
    ErrNotExist   = errNotExist()   // "file does not exist"
    ErrClosed     = errClosed()     // "file already closed"
)
  • Syntax
package regexp

// An Op is a single regular expression operator.
//
//go:generate stringer -type Op -trimprefix Op
type Op uint8
  • Heading
// Package strconv implements conversions to and from string representations
// of basic data types.
//
// # Numeric Conversions
//
// The most common numeric conversions are [Atoi] (string to int) and [Itoa] (int to string).
...
package strconv
  • Links
// Package json implements encoding and decoding of JSON as defined in
// [RFC 7159]. The mapping between JSON and Go values is described
// in the documentation for the Marshal and Unmarshal functions.
//
// For an introduction to this package, see the article
// “[JSON and Go].”
//
// [RFC 7159]: https://tools.ietf.org/html/rfc7159
// [JSON and Go]: https://golang.org/doc/articles/json_and_go.html
package json
  • Doc links
package bytes

// ReadFrom reads data from r until EOF and appends it to the buffer, growing
// the buffer as needed. The return value n is the number of bytes read. Any
// error except [io.EOF] encountered during the read is also returned. If the
// buffer becomes too large, ReadFrom will panic with [ErrTooLarge].
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
    ...
}
  • Lists
package url

// PublicSuffixList provides the public suffix of a domain. For example:
//   - the public suffix of "example.com" is "com",
//   - the public suffix of "foo1.foo2.foo3.co.uk" is "co.uk", and
//   - the public suffix of "bar.pvt.k12.ma.us" is "pvt.k12.ma.us".
//
// Implementations of PublicSuffixList must be safe for concurrent use by
// multiple goroutines.
//
// An implementation that always returns "" is valid and may be useful for
// testing but it is not secure: it means that the HTTP server for foo.com can
// set a cookie for bar.com.
//
// A public suffix list implementation is in the package
// golang.org/x/net/publicsuffix.
type PublicSuffixList interface {
    ...
}
  • Code blocks
package sort

// Search uses binary search...
//
// As a more whimsical example, this program guesses your number:
//
//  func GuessingGame() {
//      var s string
//      fmt.Printf("Pick an integer from 0 to 100.\n")
//      answer := sort.Search(100, func(i int) bool {
//          fmt.Printf("Is your number <= %d? ", i)
//          fmt.Scanf("%s", &s)
//          return s != "" && s[0] == 'y'
//      })
//      fmt.Printf("Your number is %d.\n", answer)
//  }
func Search(n int, f func(int) bool) int {
    ...
}

3. 生成文档

本地启动文档服务器:

godoc -http :8080

访问 http://localhost:8080/ 即可查看包的文档。

编写示例代码

为了方便用户了解如何使用发布的 go 包,往往需要在文档中添加示例函数即 example code,godoc 提供了一种基于测试套件示例函数编写方法。与典型的测试一样,示例函数也是定义在包的 _test.go 文件中的函数。不过与普通测试函数不同的是,示例函数不带参数并且以单词 Example 而不是 Test 开头,运行 go 测试 go test -v ./... 即可完成对示例函数编译,godoc 就可以在文档中展示相关函数的示例代码了,如 edony-ink/log 中的代码所示。

另外一个要注意点的地方是:example 函数中 Output 注释,当执行示例时,测试框架捕获写入标准输出的数据,然后将输出与示例的“Output:”注释进行比较。如果测试的输出与其输出注释匹配,则测试通过。如果没有 “Output:” 注释的话,示例函数被编译但不执行,没有输出注释的示例对于演示无法作为单元测试运行的代码(例如访问网络的代码)非常有用,同时保证示例至少可以编译。

示例函数的命名规则:

func ExampleFoo()     // documents the Foo function or type
func ExampleBar_Qux() // documents the Qux method of type Bar
func Example()        // documents the package as a whole

pkg.go.dev 信息刷新

go 包发布之后会碰到 pkg.go.dev 上没有刷新的问题,根据 go/issue38848 可以强制触发信息刷新,方法如下:

1. 假设发布的包名是:github.com/edony-ink/log
2. 假设发布的包最新版本是:v1.0.1
3. 浏览器访问: https://proxy.golang.org/github.com/edony-ink/log/@v/v1.0.1.info

3. RESTful API 文档

go 研发中经常会碰到 RESTful API 文档管理的问题,swagger 框架可以帮助解决,不过这个跟发布 go 包关系不大,这篇文章就不再赘述了,后面再行补上。

References

  1. Publishing a module - The Go Programming Language
  2. Go Doc Comments - The Go Programming Language
  3. Module release and versioning workflow - The Go Programming Language
  4. amalmadhu06/godoc-example: How to use godoc for documenting your Go packages? Explained with examples
  5. Testable Examples in Go - The Go Programming Language
  6. Monkey Patching in Go
  7. go.dev: allow force-refresh of package just like on godoc.org · Issue #38848 · golang/go

Public discussion

足迹