前言

本文翻译自:The Magic of Go Comments,对原文做了更新和补充。

在俺看来,Go 使用注释做特殊处理的设计不是很好,注释应当只做注释这一件事,不应该承担更多职责。俺比较推崇 Java 使用注解的做法,不破坏代码结构,也比较清晰。


注释是记录和交流代码信息很有价值的工具。它们是几乎所有编程语言的基本特征。Go 也不例外。然而,Go 中的注释远不止提供代码信息阅读器。在本文中我将重点介绍 Go 中注释鲜为人知的用法,它们具有特殊的甚至神奇的行为。

Go 注释语法

继承其在 C 中的语法传统,Go 支持使用 // 和 /* … */ 的熟悉的单行和多行注释,如下所示:

// 这是一行注释,增加一行注释需要 // 标记

var foo int;    // 注释可以从一行的任何地方开始

/* 在结束标记前都是注释
   新的一行注释

   空行 */

/* 多行注释也可以用在单行中 */

即使对 Go 没有太多编程经验的人,也会从他们阅读过的代码甚至他们自己编写的代码中识别出这种注释语法。然而,虽然注释的内容被 Go 编译器忽略了,但这并不意味着它被 Go 工具集完全忽略了。本文的提醒将展示一组特殊格式的注释以及它们在 Go 中的使用方式。

godoc 文档

Go 中最为人知的“神奇”注释形式可能是 Go 内置文档工具 godoc。 godoc 的工作原理是扫描包中的所有 .go 文件(忽略任何 _test.go 文件)以查找紧接在声明之前的注释(没有任何中间代码或空行)。 godoc 将使用注释的文本来形成包的文档。

例如,要记录一个函数,我们只需在其声明之前的行上放置一行或多行注释:


// Foo 将会处理入参字符串,如果字符串不能被处理将会返回错误
func Foo(s string) error {
    ...
}

在任何可导出的包级别的类型、函数、方法、变量或常量声明之前都可以使用相同的注释样式:

package objects

// Object 是一个通用的东西。
type Object struct {}

// Bar 将禁止对象,如果不可禁止则返回错误。
func (o Object) Bar() error {
    return nil
}

// List 包含已注册的对象。
var List []Object

// MaxCount 决定被允许对象的最大数量
const MaxCount = 50

因为只处理导出的包级注释,开发人员可以自由地在方法/函数体中使用注释,而不必担心注释被无意添加到公共文档中。

godoc 还提供了一种通过解析在包声明之前找到的任何注释来生成包级文档的方法:

// 包注释为起始注释
package objects

需要注意的是,godoc 在生成其包索引时使用包文档注释的第一句(示例参见 https://golang.org/pkg/),因此请务必相应地编写包描述。

如你所见,注释允许提供一种非常简单的方法来提供开发人员和用户文档,而无需复杂的语法或额外的文档文件。

构建约束

Go 中注释的第二个特殊用途是构建约束。

Go 作为一种编程语言的一个关键特性是它支持各种操作系统和架构。通常相同的代码可以用于多个平台,但在某些情况下,有操作系统或架构特定的代码应该只用于某些目标。标准的 go 构建工具可以通过理解以 OS 名称 and/or 架构结尾的程序只应用于匹配这些标签的目标来处理一些此类情况。例如,一个名为 foo_linux.go 的文件将只为 Linux 操作系统编译,foo_amd64.go 用于 AMD64 架构,而 foo_windows_amd64.go 用于在 AMD64 架构上运行的 64 位 Windows 系统。

但是,这些命名约定在更复杂的情况下会失效,例如,当相同的代码可用于多个(但不是全部)操作系统时。对于这些情况,Go 具有构建约束的概念 - 由 go build 读取的特定注释以确定在编译 Go 程序时要引用哪些文件。

在1.17版本之前,Go 的构建约束使用 // +build 做标记,现在已经逐渐被 //go:build 取代。具体细节参见:Bug-resistant build constraints — Draft Design

此处不再翻译老版本的构建约束,俺按照原作者的行文,使用 //go:build 讲解。

构建约束是遵循以下规则的注释:

  • 注释以//go:build开头
  • 位于包声明之前的文件顶部
  • 和包声明之间需要空行

例如:

//go:build linux

package main
...

当要对多个平台或操作系统进行构建约束时,我们可以使用 ||(或), &&(与),!(非)做条件限定。

比如允许 linux 或 darwin 编译:

//go:build linux || darwin

package main
...

表示兼容linux 和 darwin平台

//go:build linux && darwin

package main
...

不允许 windows

//go:build !windows

package main
...

生成代码

Go 中注释的另一个有趣的替代用途是通过 go generate 命令生成代码。 go generate 是标准 Go 工具包的一部分,通过运行用户指定的外部命令以编程方式生成源(或其他)文件。 Go 通过扫描 .go 程序以查找包含要运行的命令的特殊注释然后执行它们来生成代码。

具体来说,go generate 会查找以 go:generate 开头的注释(注释标记和文本开始之间没有空格),如下所示:

//go:generate <command> <arguments>
...

与构建约束不同,go:generate 注释可以位于 .go 源文件中的任何位置(尽管典型的 Go 习惯用法是将它们放在文件开头附近)。

go generate 的一个常见用途是通过此处提供的 stringer 工具提供数字常量的人类可读形式。 stringer 文档提供了以下示例来解释其操作。 给定一个带有枚举常量的自定义类型 Pill:

type Pill int

const (
  Placebo Pill = iota
  Aspirin
  Ibuprofen
  Paracetamol
  Acetaminophen = Paracetamol
)

运行命令 stringer -type Pill 将创建一个新的源文件,pil_string.go,它提供以下方法:

func (p Pill) String() string

例如,它允许打印 const 的名称,例如:

fmt.Printf("pill type: %s", pill)

但是需要记住包中每个适用类型的正确命令和参数可能很复杂,因此我们可以将以下注释添加到包中的 .go 文件中:

//go:generate stringer -type=Pill
...

然后运行 go generate 将使用正确的参数触发 stringer 调用,以创建我们的 Pill 字符串方法。 鉴于此,我们可以看到 stringer 和 go generate 如何为程序员带来很大好处,尤其是在包中有多个自定义类型的情况下。

Cgo

我将讨论的 Go 中注释的最后一个特殊用途是 C 集成工具 Cgo。 Cgo 允许 Go 程序直接调用 C 代码,允许在 Go 中重用已建立的 C 库。 要在 Go 程序中使用 Cgo,首先要导入伪包“C”。 一旦导入,Go 程序就可以引用 C.size_t 等原生 C 类型和 C.putchar() 等函数。

然而,C 语言编程的某些方面并不容易转化为 Go。 为了处理这些,Cgo 特别使用了紧接在 import “C” 语句之前的注释(在 Cgo 术语中称为序言)作为提供各种 C 特定配置项的方法。

其中一项是#include 指令。 几乎每个 C 程序都需要 #include 指令来指示头文件的位置。 Go 语言没有任何本机等效命令(导入适用于包,而不是头文件),因此 Cgo 会解析序言中的 #include 语句。 例如:

// #include <stdio.h>
// #include <errno.h>
import "C"

序言注释不仅限于#include 语句。 事实上,import 语句之前的任何注释都将被视为标准 C 代码,然后可以通过 C 包被 Go 引用。 例如,在序言中:

// #include <stdio.h>
//
// static void myprint(char *s) {
//     printf("%s\n", s)
// }
import "C"

然后我们可以在 Go 中引用这个新定义的 C 函数,如下所示:

C.myprint(C.String("foo"))

最后,为了处理编译器和类似选项,Cgo 引入了 #cgo 指令,该指令可用于设置环境变量、编译器标志和运行 pkg-config 命令,如下所示:

// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #cgo pkg-config: png cairo
// #include <png.h>
import "C"

所有这些序言定义都有助于使使用 Cgo 的程序(几乎)与 go 构建工具无缝集成,而不需要额外的 makefile 或其他脚本的复杂性。

文件嵌入二进制

Go 1.16 版本后新增 //go:embed,用于将文件内嵌如 Go 打包二进制。本站之前有做过介绍,参见:Golang 将文件嵌入二进制文件 这里不再赘述。

结论

我写这篇文章是为了向新手 Go 程序员介绍一些鲜为人知的注释用法。 我希望你觉得它有趣且有用。 有关这些概念的更多详细信息,请访问以下链接: