设为首页 加入收藏

TOP

Go 终极指南:编写一个 Go 工具(一)
2017-10-28 06:06:45 】 浏览:1117
Tags:终极 指南 编写 一个 工具

https://arslan.io/2017/09/14/the-ultimate-guide-to-writing-a-go-tool/
作者:Fatih Arslan
译者:oopsguy.com

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

在 vim-go 中使用 gomodifytags 的一个示例

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

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

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


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

  1. 它需要读取源文件、理解并能够解析 Go 文件
  2. 它需要找到相关的结构体
  3. 找到结构体后,它需要获取字段名称
  4. 它需要根据字段名来更新结构体标签(根据转换规则,如 snake_case
  5. 它需要能够把这些更改更新到文件中,或者能够以可消费的方式输出更改后的结果

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

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

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

  • 一个结构体标签是一个字符串文字(因为它有字符串类型)
  • 键(key)部分是一个无引号的字符串文字
  • 值(value)部分是带引号的字符串文字
  • 键和值由冒号(:)分隔。键与值且由冒号分隔组成的值称为键值对
  • 结构体标签可以包含多个键值对(可选)。键值对由空格分隔
  • 不是定义的部分是选项设置。像 encoding/json 这样的包在读取值时当作一个由逗号分隔列表。 第一个逗号后的内容都是选项部分,比如 foo,omitempty,string。其有一个名为 foo 的值和 [omitempty, string] 选项
  • 因为结构体标签是字符串文字,所以需要使用双引号或反引号包围。因为值必须使用引号,因此我们总是使用反引号对整个标签做处理。

总的来说:

结构体标签定义有许多隐藏的细节

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

package main

import (
    "fmt"
    "reflect"
)

func main() {
    tag := reflect.StructTag(`species:"gopher" color:"blue"`)
    fmt.Println(tag.Get("color"), tag.Get("species"))
}

结果:

blue gopher

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

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

  • 它无法检测到标签是否格式错误(如:键部分用引号包裹,值部分没有使用引号等)。
  • 它无法得知选项的语义
  • 它没有办法迭代现有的标签或返回它们。我们必须要知道要修改哪些标签。如果不知道名字怎么办?
  • 修改现有标签是不可能的。
  • 我们不能从头开始构建新的结构体标签

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

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

package main

import (
    "fmt"

    "github.com/fatih/structtag"
)

func main() {
    tag := `json:"foo,omitempty,string" xml:"foo"`

    // parse the tag
    tags, err := structtag.Parse(string(tag))
    if err != nil {
        panic(err)
    }

    // iterate over all tags
    for _, t := range tags.Tags() {
        fmt.Printf("tag: %+v\n", t)
    }

    // get a single tag
    jsonTag, err := tags.Get("json")
    if err != nil {
        panic(err)
    }

    // change existing tag
    jsonTag.Name = "foo_bar"
    jsonTag.Options = nil
    tags.Set(jsonTag)

    // add new tag
    tags.Set(&structtag.Tag{
        Key:     "hcl",
        Name:    "foo",
        Options: []string{"squash"},
    })

    // print the tags
    fmt.Println(tags) // Output: json:"foo_bar" xml:"foo" hcl:"foo,squash"
}

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

答案是通过 AST。AST(Abstract Syntax Tree,抽象语法树)允许我们从源代码中检索每个标识符(节点)。 下面你可以看到一个结构体类型的 AST(简化版):

一个基本的 Go ast.Node 表示形式的结构体类型

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

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

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

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/tok
首页 上一页 1 2 3 4 5 6 下一页 尾页 1/6/6
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇go 代码的调试---打印调用堆栈 下一篇程序员如何打造属于自己的云笔记..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目