开发人员有很强的你会吗自动化重复性任务的倾向,这也适用于编写代码。完全因此,指南元编程(metaprogramming)的你会吗主题是一个开发和研究的热门领域,可以追溯到 1960 年代的完全 Lisp。元编程中一个特别有用的指南领域是代码生成(code-generation)。支持宏的你会吗语言内置了此功能;其他语言扩展了现有功能以支持这一点(例如 C++模板元编程[1])。 虽然 Go 没有宏或其他形式的完全元编程,但它是指南一种实用语言,它包含官方工具链支持的你会吗代码生成。 自从 Go 1.4[2] 引入 go generate 命令后,完全它一直广泛应用于 Go 生态系统。指南Go 项目本身在很多地方都依赖于 go generate;我将在后面的你会吗帖子中快速概述这些用例。 让我们从一些术语开始。完全go generate 工作方式主要由三个参与者之间协调进行的指南: 需要强调的是,以上是 Go 为代码生成提供的自动化的全部范围。对于其他任何事情,开发人员可以自由使用适合他们的任何工作流程。例如,go generate 应该始终由开发人员手动运行;它永远不会自动调用(比如不会作为 go build 的一部分)。此外,由于我们通常使用 Go 将二进制文件发送给用户或执行环境,因此很容易理解 go generate 仅在开发期间运行(可能就在运行 go build 之前);Go 程序的用户不会知道哪部分代码是生成的以及如何生成的高防服务器。(实际上,很多时候会在生成的文件开头加上注释,这是生成的,请别手动修改。) 这也适用于生成 module;go generate 不会运行导入包的生成器。因此,当一个项目发布时,生成的代码应该与其余代码一起 checked 和分发。 学习最好是动手做;为此,我创建了几个简单的 Go 项目,它们将帮助我说明这篇文章中解释的主题。第一个是samplegentool[3],一个基本的 Go 工具,用于模拟生成器。 这是它的完整源代码: 这个工具不读任何代码,也不写任何代码;它所做的只是报告它是如何被调用的亿华云。我们很快就会了解细节。首先我们看另一个项目 - mymod[4]。这是一个示例 Go 模块,包含 3 个文件,分为两个包: 这些文件的内容只是填充物;重要的是 go:generate 这个神奇的注释。让我们以mypack/mypack.go 中的那个为例: 我们看到它调用带有一些参数的 samplegentool。为了使这个调用起作用,应该在 PATH 的某个地方能找到 samplegentool。这可以通过在 samplegentool项目运行 go build 来完成,以生成二进制,然后设置 PATH。现在,如果我们在 mymod 项目的根目录中运行 go generate ./...,我们将看到如下内容: 首先,注意 samplegentool 在它出现在 magic comment 中的每个文件上被调用;这包括子目录,因为我们 使用 ./... 模式运行 go generate。这对于在不同地方有很多生成器的大型项目来说真的很方便。 输出中有很多有趣的东西;让我们一行一行地剖析它: 现在我们已经很好地了解了 go generate 是如何调用生成器的,那么它们能做什么呢?事实上他们可以做任何我们想做的事情。毕竟,生成器是计算机程序。如前所述,生成的文件通常也会放入到源代码中,因此生成器可能只需要很少次运行。在许多项目中,开发人员不会像我在上面的示例中那样从根运行 go generate ./...;相反,他们只会根据需要在特定目录中运行特定的生成器。 在下一节中,我将深入介绍一个非常流行的生成器 — stringer工具。同时,以下是 Go 项目本身使用生成器执行的一些任务(这不是完整列表;所有用途都可以通过在 Go 源代码树中 grepping go:generate 找到): 此外,标准库中至少有两个地方使用生成器来实现类似泛型的功能,其中几乎重复的代码是从不同类型的现有代码中生成的,比如 sort 和 suffixarray 包。 Go 项目中最常用的生成器之一是stringer[6] — 一种自动为类型创建 String() 方法的工具,以便它们实现 fmt.Stringer 接口。它最常用于为枚举生成文本表示。 我们看标准库math.big 包中的一个例子;具体来说是 RoundingMode[7] 类型,其定义如下: 至少在 Go 1.18 之前,这是一个惯用的 Go 枚举;为了使这些枚举值的名称可打印,我们需要为这种类型实现一个 String() 方法,这会使用 switch 语句,枚举每个值及其字符串表示。这是一项非常重复的工作,stringer 工具正好派上用场。 我在一个小示例模块中[8]复制了 RoundingMode 类型及其值, 以便我们可以更轻松地试验生成器。让我们在文件中添加适当的 magic 注释: 我们将快速讨论 stringer 接受的 flag。确保先安装了它: 现在我们可以运行 go generate;因为在示例项目中,带有 magic 注释的文件位于一个子包中,所以我将从模块根目录运行它: 如果一切设置正确,此命令成功完成后不会有任何输出。查看项目内容,会发现生成了一个名为roundingmode_string.go 的文件,内容如下: 工具 stringer 拥有多个代码生成策略,取决于调用它的枚举值的性质。我们的案例是最简单的案例,其中包含“单次连续运行(single consecutive run)”的值。如果这些值形成多个连续运行,stringer 将生成稍微不同的代码,如果这些值根本不形成运行,则生成另一个版本。为了娱乐和讲解,详细研究 stringer 的来源;在这里,让我们关注当前使用的策略。 首先,_RoundingMode_name 常量用于有效地将所有字符串表示形式保存在单个连续字符串中。_RoundingMode_index 用作此字符串的查找表;例如 ToZero 值为 2。_RoundingMode_index[2] 是 26,所以该代码将索引_RoundingMode_name在索引 26 中,这使我们的ToZero部(端是下一个索引,32 在这种情况下) . 因此,代码将索引到索引 26 处的 _RoundingMode_name,这将引导我们找到 ToZero 部分。 String() 中的代码有一个回调函数,以防添加更多枚举值但未重新运行 stringer 工具。在这种情况下,产生的值将是 RoundingMode(N),其中 N 是数值。 这个回调很有用,因为 Go 工具链中没有任何内容可以保证生成的代码与源代码保持同步;如前所述,运行生成器完全是开发人员的责任。 但是 func _() 中的奇怪代码呢?首先,请注意它实际上什么也没有编译:该函数不返回任何内容,没有副作用并且不会被调用。这个函数的目的是作为 编译守卫;如果原始 enum 以与生成的代码根本不兼容的方式发生变化,并且开发人员忘记重新运行 go generate,则这是一种额外的安全性。具体来说,它将防止现有的枚举值被修改。在这种情况下,除非重新运行 go generate,否则 String() 方法可能会成功,但会产生完全错误的值。编译守卫试图通过使代码无法编译越界数组查找来捕获这种情况。 现在让我们谈谈 stringer 的工作原理;首先,阅读它的 -help 是有指导意义的: 我们已经使用 -type 参数告诉 stringer 为哪种类型生成 String() 方法。在现实的代码库中,人们可能希望在其中定义了多种类型的包上调用该工具;在这种情况下,我们可能希望stringer 只为特定类型生成 String() 方法。 我们没有指定 -output flag,所以使用默认值;在这种情况下,生成的文件名为 roundingmode_string.go。 眼尖的读者会注意到,当我们调用 stringer 时,我们没有指定它应该用作输入的文件。快速浏览该工具的源代码会发现它也不使用 GOFILE 环境变量。那么它如何知道要分析哪些文件呢?事实证明,stringer 使用 golang.org/x/tools/go/packages 从其当前工作目录(你还记得,这是包含 magic 注释的文件所在的目录)加载整个包。这意味着无论魔术(magic)注释在哪个文件中,stringer 默认情况下会分析整个包。如果你仔细考虑一下,这是有道理的,谁说常量必须与类型声明在同一个文件中?在 Go 中,文件只是一个方便的代码容器;包是工具关心的真正输入单位。 到目前为止,我们假设生成器在 go generate 运行时位于 PATH 中的某个位置,但情况并非总是如此。 考虑一个非常常见的场景,你的模块有自己的生成器,它只对这个特定的模块有用。当有人对模块进行黑客攻击时,他们能够克隆代码,运行 go generate 和 go build 等。但是,如果魔术注释假定生成器始终位于 PATH 中,则除非在运行 go generate 之前构建并正确指向生成器,否则这将无法工作。 Go 中的解决方案很简单,因为 go run 是运行生成器的完美搭配,这些生成器只是模块树中某处的 .go 文件。这里有[9]一个简单的例子。这是一个带有神奇注释的包文件: 请注意此处如何调用生成器:使用 go run gen.go。这意味着 go generate 将期望在与包含魔术注释的文件相同的目录中找到 gen.go。gen.go 的内容是: 它只是一个小的 Go 程序(在包 main 中)。唯一需要注意的是 //go:build 约束,它告诉 Go 工具链在构建项目时忽略这个文件。事实上,gen.go 不是包的一部分;它位于 main 包中,旨在与 go generate 一起运行,而不是编译到包中。 标准库中有许多小程序的示例,这些小程序旨在通过作为生成器的 go run 调用。 典型的模式是代码生成涉及 3 个文件,它们都共存于同一个目录/包中: 当然,这些都不是工具所要求的——它只是描述了一个通用的约定;特定的项目可以以不同的方式设置(例如,一个生成器为多个包生成代码)。 本节讨论 go generate 的一些高级或较少使用的功能。 -command 标志 这个 flag 让我们为 go:generate 行定义别名;如果某些生成器是一个多字命令,我们想为多次调用缩短它,这可能会很有用。 最初的动机可能是将 go tool yacc 缩短为 yacc : 之后 yacc 可以只用这个 4 个字母的名字而不是三个词来调用多次。 有趣的是,go tool yacc在 1.8 中[10]从核心 Go 工具链中删除了,而且我在主 Go 存储库(除了测试go generate本身)或x/tools模块中都没有发现 -command 的任何用法 。 -run 标志 该标志用于 go generate 命令本身,用于选择要运行的生成器。回想一下我们在同一个项目中调用了 3 次 samplegentool 的简单示例 。我们只能选择其中之一来使用 -run 标志运行: 这对于调试应该是显而易见的:在具有多个生成器的大型项目中,我们通常只想运行一个子集以进行调试/快速编辑这样的循环目的。 DOLLAR 在自动神奇地传递给生成器的环境变量( env var )中,有一个脱颖而出 —— DOLLAR。它是做什么用的?为什么要将 env var 专用于一个字符?在 Go 源代码树中没有使用这个 env var。 DOLLAR的起源可以追溯到Rob Pike 的这个提交[11]。正如更改描述所说,这里的动机是将 $ 字符传递到生成器中,而无需复杂的shell escaping[12]。如果 go generate 调用 shell 脚本或将正则表达式作为参数的东西,这很有用。 可以使用我们的 samplegentool 生成器观察 DOLLAR 的效果。如果我们将其中一个神奇的注释更改为: 生成器报告其参数为 这是因为 $somevar 被 shell 解释为引用 somevar 变量,该变量不存在,因此其默认值为空。相反,我们可以如下使用 DOLLAR: 然后生成器报告: 原文链接:https://eli.thegreenplace.net/2021/a-comprehensive-guide-to-go-generate/ [1]C++模板元编程: https://en.wikipedia.org/wiki/Template_metaprogramming [2]Go 1.4: https://go.dev/blog/generate [3]samplegentool: https://github.com/eliben/code-for-blog/tree/master/2021/go-generate-guide/samplegentool [4]mymod: https://github.com/eliben/code-for-blog/tree/master/2021/go-generate-guide/mymod [5]官方文档: https://pkg.go.dev/cmd/go#hdr-Generate_Go_files_by_processing_source [6]stringer: https://pkg.go.dev/golang.org/x/tools/cmd/stringer [7]RoundingMode: https://pkg.go.dev/math/big#RoundingMode [8]小示例模块中: https://github.com/eliben/code-for-blog/tree/master/2021/go-generate-guide/stringerusage [9]这里有: https://github.com/eliben/code-for-blog/tree/master/2021/go-generate-guide/insourcegenerator [10]在 1.8 中: https://tip.golang.org/doc/go1.8#tool_yacc [11]Rob Pike 的这个提交: https://go-review.googlesource.com/c/go/+/8091/ [12]shell escaping: http://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Quoting01 基础知识
02 一个简单的例子
03 generators(生成器) 能做什么?
04 深挖生成器 stringer
05 源码生成器和构建 tags
06 高级功能
参考资料