雷州房产网

当前位置: 首页 >数据

Go终极指南编写一个Go工具

来源: 作者: 2019-11-10 02:41:05

https://arslan.io/2017/09/14/the-ultimate-guide-to-writing-a-go-tool/

作者:Fatih Arslan译者:oopsguy.com

我之前编写过一个叫gomodifytags的工具,它使我的生活变得很轻松。它会根据字段名称自动填充结构体标签字段。让我来展现一下它的功能:

Go终极指南编写一个Go工具

使用这样的工具可以很容易管理结构体的多个字段。该工具还可以添加和删除标签、管理标签选项(如 )、定义转换规则( 、 等)等。但该工具是怎样工作的呢?它内部使用了甚么 Go 包?有很多问题需要回答。

这是一篇非常长的博文,其解释了如何编写这样的工具和每个构建细节。它包含许多独特的细节、技能和未知的 Go 知识。

拿起一杯咖啡,让我们深入一下吧!

首先,让我列出这个工具需要做的事情:

它需要读取源文件、理解并能够解析 Go 文件

它需要找到相干的结构体

找到结构体后,它需要获取字段名称

它需要根据字段名来更新结构体标签(根据转换规则,如 )

它需要能够把这些更改更新到文件中,或者能够以可消费的方式输出更改后的结果

我们首先来了解什么是结构体(struct)标签(tag),从这里我们可以学习到所有东西和如何把它们组合在一起使用,在此基础上您可以构建出这样的工具。

Go终极指南编写一个Go工具

结构体的标签值(内容,如 )不是官方规范的一部分,但是 包定义了一个非官方规范的格式标准,这个格式一样被 包(如 )所使用。它通过 reflect.StructTag 类型定义:

Go终极指南编写一个Go工具

这个定义有点长,不是很容易让人理解。我们尝试分解一下它:

一个结构体标签是一个字符串文字(由于它有字符串类型)

键(key)部分是一个无引号的字符串文字

值(value)部份是带引号的字符串文字

键和值由冒号(:)分隔。键与值且由冒号分隔组成的值称为键值对

结构体标签可以包括多个键值对(可选)。键值对由空格分隔。

不是定义的部份是选项设置。像 这样的包在读取值时当作一个由逗号分隔列表。 第一个逗号后的内容都是选项部分,比如 。其有一个名为 的值和 [ ] 选项

由于结构体标签是字符串文字,所以需要使用双引号或反引号包围。由于值必须使用引号,因此我们总是使用反引号对全部标签做处理。

总的来说:

我们已了解了什么是结构体标签,我们可以根据需要轻松地修改它。 现在的问题是,我们如何解析它才能使我们能够轻松进行修改?荣幸的是, 包含一个方法,它允许我们进行解析并返回指定键的值。以下是一个示例:

结果:

如果键不存在,则返回一个空字符串。

这是非常有用,但也有一些不足使得它其实不合适我们,因为我们需要更多的灵活性:

它无法检测到标签是否是格式错误(如:键部分用引号包裹,值部分没有使用引号等)。

它没法得知选项的语义。

它没有办法迭代现有的标签或返回它们。我们必须要知道要修改哪些标签。如果不知道名字怎么办?

修改现有标签是不可能的。

我们不能从头开始构建新的结构体标签。

为了改进这一点,我写了一个自定义的 Go 包,它解决了上面提到的所有问题,并提供了一个 ApI,可以轻松地改变结构体标签的各个方面。

该包名为structtag,可以从 github.com/fatih/structtag 获得。 这个包允许我们以简洁的方式解析和修改标签。以下是一个完整的示例,您可以复制/粘贴并自行尝试:

现在我们了解了如何解析、修改或创建结构体标签,是时候尝试修改一个 Go 源文件了。在上面的示例中,标签已经存在,但是如何从现有的 Go 结构体中获取标签呢?

在这棵树中,我们可以检索和操作每一个标识符、每个字符串、每一个括号等。这些都由 AST 节点表示。例如,我们可以通过替换表示它的节点将字段名称从 更改成 。 该逻辑一样适用于结构体标签。

要获得一个 Go AST,我们需要解析源文件并将其转换成一个 AST。实际上,这两者都是通过同一个步骤来处理的。

要实现这一点,我们将使用 go/parser 包来解析文件以获取 AST(全部文件),然后使用 go/ast 包来处理整个树(我们可以手动做这个工作,但这是另外一篇博文的主题)。 您在下面可以看到一个完全的例子:

输出结果:

代码执行以下操作:

我们使用一个单独的结构体定义了一个 Go 包示例

我们使用go/parser包来解析这个字符串。 包也可以从磁盘读取文件(或全部包)。

在解析后,我们处理了节点(分配给变量文件)并查找由 ast.StructType 定义的 AST 节点(参考 AST 图)。通过 函数完成树的处理。它会遍历所有节点,直到它收到 false 值。 这是非常方便的,由于它不需要知道每个节点。

我们打印了结构体的字段名称和结构体标签。

我们现在可以做两件重要的事,首先,我们知道了如何解析一个 Go 源文件并检索结构体标签(通过 )。其次,我们知道了如何解析 Go 结构体标签,并根据需要进行修改(通过 github.com/fatih/structtag)。

我们有了这些,现在可以通过使用这两个知识点开始构建我们的工具(命名为gomodifytags)。该工具应按顺序履行以下操作

获取配置,用于告知我们要修改哪一个结构体

根据配置查找和修改结构体

输出结果

由于gomodifytags将主要应用于编辑器,我们将通过 CLI 标志传入配置。第二步包括多个步骤,如解析文件,找到正确的结构体,然后修改结构体(通过修改 AST)。最后,我们将结果输出,无论结果的格式是原始的 Go 源文件还是某种自定义协议(如 JSON,稍后再说)。

以下是简化版 gomodifytags 的主要功能:

让我们更详细地解释每个步骤。为了简单起见,我将尝试以概括的情势来解释重要部份。 原理都一样,一旦你读完这篇博文,你将能够在没有任何指点情况下阅全部源码(指南末尾附带了所有资源)

让我们从第一步开始,了解如何取得配置。以下是我们的配置,包括所有必要的信息

它分为三个主要部分:

第一部分包括有关如何读取和读取哪一个文件的设置。这可以是本地文件系统的文件名,也可以直接来自 stdin(主要用在编辑器中)。 它还设置如何输出结果(go 源文件或 JSON),和是否是应当覆盖文件而不是输出到 stdout。

第二部分定义了如何选择一个结构体及其字段。有多种方法可以做到这一点。 我们可以通过它的偏移(光标位置)、结构体名称、一行单行(仅选择字段)或一系列行来定义它。最后,我们无论如何都得到开始行/结束行。例如在下面的例子中,您可以看到,我们使用它的名字来选择结构体,然后提取开始行和结束行以选择正确的字段:

如果是用于编辑器,则最好使用字节偏移量。例如下面你可以发现我们的光标刚好在 字段名称后面,从那里我们可以很容易地得到开始行/结束行:

配置中的第三个部份实际上是一个映射到 包的一对一映照。它基本上允许我们在读取字段后将配置传给 包。 如你所知, 包允许我们解析一个结构体标签并对各个部分进行修改。但它不会覆盖或更新结构体字段。

我们如何获得配置?我们只需使用 包,然后为配置中的每一个字段创建一个标志,然后分配它们。举个例子:

我们对配置中的每个字段实行相同操作。有关完全内容,请查看 gomodifytag 当前 master 分支的标志定义

一旦我们有了配置,就可以做些基本的验证:

将验证部份放置在一个单独的函数中,以便测试。现在我们了解了如何取得配置并进行验证,我们继续解析文件:

我们已开始讨论如何解析文件了。这里的解析是 结构体的一个方法。实际上,所有的方法都是 结构体的一部分:

parse函数只做一件事:解析源代码并返回一个 。如果我们传入的是文件,那就非常简单了,在这种情况下,我们使用 parser.parseFile() 函数。需要注意的是 ,它创建一个 类型。我们将它存储在 中,同时也传给了 函数。为何呢?

由于fileset用于为每个文件独立存储每个节点的位置信息。这在以后非常有用,可以用于取得 的确切位置(请注意, 使用了一个压缩了的位置信息 。要取得更多的信息,它需要通过 函数来获取一个 ,其包含更多的信息)

让我们继续。如果通过 stdin 传递源文件,那末这更加有趣。 字段是一个易于测试的 ,但实际上我们传递的是 stdin。我们如何检测是不是需要从 stdin 读取呢?

我们询问用户是不是想通过 stdin 传递内容。这种情况下,工具用户需要传递 标志(这是一个布尔标志)。如果用户了传递它,我们只需将 stdin 分配给 :

如果再次检查上面的 函数,您将发现我们检查是不是已分配了 字段。由于 stdin 是一个任意的数据流,我们需要能够根据给定的协议进行解析。在这种情况下,我们假定存档包括以下内容:

文件名,后接一行新行

文件大小(十进制),后接一行新行

文件的内容

由于我们知道文件大小,可以无障碍地解析文件内容。任何超出给定文件大小的部份,我们仅仅停止解析。

此方法也被其他几个工具所使用(如guru、gogetdoc等),对编辑器来说非常有用。 由于这样可以让编辑器传递修改后的文件内容,而不会保存到文件系统中。因此命名为modified。

现在我们有了自己的节点,让我们继续 “搜索结构体” 这一步:

在 main 函数中,我们将使用从上一步解析得到的 调用 函数:

函数根据配置返回结构体的开始位置和结束位置以告知我们如何选择一个结构体。它迭代给定节点,然后返回开始位置/结束位置(如上配置部份中所述):

但是怎么做呢?记住有三种模式。分别是行选择、偏移量和结构体名称:

行选择是最简单的部份。这里我们只返回标志值本身。因此如果用户传入 标志,函数将返回 。 它所做的就是拆分标志值并将其转换为整数(同样履行验证):

当您选中一组行并高亮它们时,编辑器将使用此模式。

偏移量和结构体名称选择需要做更多的工作。 对这些,我们首先需要搜集所有给定的结构体,以便可以计算偏移位置或搜索结构体名称。为此,我们首先要有一个搜集所有结构体的函数:

我们使用 函数逐渐遍历 AST 并搜索结构体。我们首先搜索 ,以便我们可以获得结构体名称。搜索 时给定的是结构体本身,而不是它的名字。 这就是为何我们有一个自定义的 类型,它保存了名称和结构体节点本身。这样在各个地方都很方便。 因为每一个结构体的位置都是唯一的,并且在同一位置上不可能存在两个不同的结构体,因此我们使用位置作为 map 的键。

现在我们拥有了所有结构体,在最后可以返回一个结构体的起始位置和结束位置的偏移量和结构体名称模式。 对偏移位置,我们检查偏移是否在给定的结构体之间:

我们使用 来收集所有结构体,以后在这里迭代。还得记得我们存储了用于解析文件的初始 么?

现在可以用它来取得每一个结构体节点的偏移信息(我们将其解码为一个 ,它为我们提供了 字段)。 我们所做的只是一个简单的检查和迭代,直到我们找到结构体(这里命名为 ):

有了这些信息,我们可以提取找到的结构体的开始位置和结束位置:

该逻辑一样适用于结构体名称选择。 我们所做的只是尝试检查结构体名称,直到找到与给定名称一致的结构体,而不是检查偏移量是否是在给定的结构体范围内:

现在我们有了开始位置和结束位置,我们终究可以进行第三步了:修改结构体字段。

在 函数中,我们将使用从上一步解析的节点来调用 函数:

这是该工具的核心。在 函数中,我们将重写开始位置和结束位置之间的所有结构体字段。 在深入了解之前,我们可以看一下该函数的大概内容:

正如你所看到的,我们再次使用 来逐步处理给定节点的树。我们重写 函数中的每一个字段的标签(更多内容在后面)。

由于传递给 的函数不会返回毛病,因此我们将创建一个毛病映照(使用 变量定义),以后在我们逐渐遍历树并处理每个单独的字段时搜集毛病。现在让我们来谈谈 的内部原理:

记住,AST 树中的每个节点都会调用这个函数。因此,我们只寻找类型为 的节点。一旦我们拥有,就可以开始迭代结构体字段。

这里我们使用 和 变量。这定义了我们是不是要修改该字段。如果字段位置位于 start-end 之间,我们将继续,否则我们将疏忽:

接下来,我们检查是不是存在标签。如果标签字段为空(也就是 nil),则初始化标签字段。这在有助于后面的 函数避免 panic:

现在让我先解释一下一个有趣的地方,然后再继续。gomodifytags尝试获得字段的字段名称并处理它。然而,当它是一个匿名字段呢?:

在这种情况下,由于没有字段名称,我们尝试从类型名称中获得字段名称:

一旦我们获得了字段名称和标签值,就可以开始处理该字段。 函数负责处理有字段名称和标签值(如果有的话)的字段。在它返回处理结果后(在我们的例子中是struct tag格式),我们使用它来覆盖现有的标签值:

实际上,如果你记得structtag,它返回标签实例的String()表述。在我们返回标签的终究表述之前,我们根据需要使用structtag包的各种方法修改结构体。以下是一个简单的说明图示:

例如,我们要扩大process()中的 函数。此功能使用以下配置来创建要删除的标签数组(键名称):

在 中,我们检查是否是使用了 。如果有,我们将使用 structtag 的 tags.Delete() 方法来删除标签:

此逻辑同样适用于 中的所有函数。

我们已有了一个重写的节点,让我们来讨论最后一个话题。输出和格式化结果:

在 main 函数中,我们将使用上一步重写的节点来调用 函数:

您需要注意的一件事是,我们输出到stdout。这佯做有许多优点。首先,您只需运行工具就能查看到结果, 它不会改变任何东西,只是为了让工具用户立即看到结果。其次,stdout 是可组合的,可以重定向到任何地方,乃至可以用来覆盖原来的工具。

现在我们来看看 函数:

我们有两种输出模式。

第一个(source)以 Go 格式打印 。这是默认选项,如果您在命令行使用它或只想看到文件中的更改,那末这非常适合您。

第二个选项(json)更加先进,其专为其他环境而设计(特别是编辑器)。它根据以下结构体对输出进行编码:

对工具进行输入和最终结果输出(没有任何毛病)大概示意图以下:

回到 函数。如之前所述,有两种模式。source 模式使用go/format包将 AST 格式化为 Go 源码。该软件包也被许多其他官方工具(如gofmt)使用。以下是source模式的实现方式:

格式包接受 并对其进行格式化。这就是为什么我们创建一个中间缓冲区( )的原因,当用户传入一个 标志时,我们可以使用它来覆盖文件。格式化后,我们返回缓冲区的字符串表示形式,其中包括格式化后的 Go 源代码。

json模式更有趣。由于我们返回的是一段源代码,因此我们需要准确地出现它本来的格式,这也意味着要把注释包含进去。问题在于,当使用 打印单个结构体时,如果它们是有损的,则没法打印出 Go 注释。

什么是有损注释(lossy comment)?看看这个例子:

每一个字段都是 类型。此结构体有一个 字段,其包含某字段的注释。

但是,在上面的例子中,它属于谁?属于foo还是bar?

由于不可能肯定,这些注释被称为有损注释。如果现在使用 函数打印上面的结构体,就会出现问题。 当你打印它时,你可能会得到(https://play.golang.org/p/peHsswF4JQ):

问题在于有损注释是 的一部分,它与树分开。只有打印全部文件时才能打印出来。 所以解决方法是打印全部文件,然后删除掉我们要在 JSON 输出中返回的指定行:

这样做确保我们可以打印所有注释。

这就是全部内容!

我们成功完成了我们的工具,以下是我们在整个指南中实行的完全步骤图:

回顾一下我们做了什么:

我们通过 CLI 标志检索配置

我们通过 包解析文件来获得一个 。

在解析文件之后,我们搜索获得相应的结构体来获取开始位置和结束位置,这样我们可以知道需要修改哪些字段

一旦我们有了开始位置和结束位置,我们再次遍历 ,重写开始位置和结束位置之间的每一个字段(通过使用 包)

以后,我们将格式化重写的节点,为编辑器输出 Go 源代码或自定义的 JSON

在创建此工具后,我收到了很多友好的评论,评论者们提到了这个工具如何简化他们的日常工作。正如您所看到,尽管看起来它很容易制作,但在整个指南中,我们已经针对许多特殊的情况做了特别处理。

gomodifytags成功应用于以下编辑器和插件已有几个月了,使得数以千计的开发人员提升了工作效率:

vim-go

atom

vscode

acme

如果您对原始源代码感兴趣,可以在这里找到:

https://github.com/fatih/gomodifytags

谢谢您阅读此文。希望这个指南能启发您从头创建一个新的 Go 工具。

感谢 DD 程序员 前辈的公众号编辑器,解决了我发布公众号文章时遇到的代码排版问题

广州西地那非

正宗美国伟哥多少钱一粒

0_10_正品印度神油

viagra100

相关推荐