• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

用Go语言编写一门工具的终极指南

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

我以前构建过一个工具,以让生活更轻松。这个工具被称为: gomodifytags ,它会根据字段名称自动填充结构体的标签字段。示例如下:


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

使用这样的工具可以 轻松管理 结构体的多个字段。该工具还可以添加和删除标签,管理标签选项(如omitempty),定义转换规则(snake_case、camelCase 等)等等。但是这个工具是如何工作的? 在后台中它究竟使用了哪些 Go 包? 有很多这样的问题需要回答。

这是一篇非常长的博客文章,解释了如何编写类似这样的工具以及如何构建它的每一个细节。 它包含许多特有的细节、提示和技巧和某些未知的 Go 位。

拿一杯咖啡,开始深入探究吧!

首先,列出这个工具需要完成的功能:

  • 它需要读取源文件,理解并能够解析 Go 文件
  • 它需要找到相关的结构体
  • 找到结构体后,需要获取其字段名称
  • 它需要根据字段名更新结构标签(根据转换规则,即:snake_case)
  • 它需要能够使用这些改动来更新文件,或者能够以可接受的方式输出改动

我们首先来看看 结构体标签的定义 是什么,之后我们会学习所有的部分,以及它们如何组合在一起,从而构建这个工具。

结构体的标签 值 (其内容,比如`json:"foo"`)并 不是官方标准的一部分 ,不过,存在一个非官方的规范,使用 reflect 包定义了其格式,这种方法也被 stdlib(例如 encoding/ json)包所采用。它是通过 reflect.StructTag 类型定义的:

结构标签的定义比较简洁所以不容易理解。该定义可以分解如下:

  • 结构标签是一个字符串(字符串类型)
  • 结构标签的 Key 是非引号字符串
  • 结构标签的 value 是一个带引号的字符串
  • 结构标签的 key 和 value 用冒号(:)分隔。冒号隔开的一个 key 和对应的 value 称为 “key value 对”。
  • 一个结构标签可以包含多个 key valued 对(可选)。key-value 对之间用空格隔开。
  • 可选设置不属于定义的一部分。类似 encoding/json 包将 value 解析为逗号分开的列表。value 的第一个逗号后面的任何部分都是可选设置的一部分,例如:“ foo, omitempty,string”。其中 value 拥有一个叫 “foo” 的名字和可选设置 [“omitempty”, "string"]
  • 由于结构标签是一个字符串,需要双引号或者反引号包含。又因为 value 也需要引号包含,经常用反引号包含结构标签。

以上规则概况如下:


(结构标签的定义有许多隐含细节)

已经了解什么是结构标签,接下来可以根据需要修改结构标签。问题来了,如何才能很容易的对所做的修改进行解析?很幸运,reflect.StructTag 包含一个可以解析结构标签并返回特定 key 的 value 的方法。示例如下:


  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "reflect" 
  6.  
  7. func main() { 
  8.     tag := reflect.StructTag(`species:"gopher" color:"blue"`) 
  9.     fmt.Println(tag.Get("color"), tag.Get("species")) 

输出:


  1. blue gopher 

如果 key 不存在则返回空串。

这是非常有帮助的, 但是 ,它有一些附加说明,使其不适合我们,因为我们需要更多的灵活性。这些是:

  • 它无法检测到标签是否存在 格式错误 (即:键被引用了,值是未引用等)
  • 它不知道选项的 语义
  • 它没有办法 迭代现有的标签 或返回它们。 我们必须知道我们要修改哪些标签。 如果不知道其名字怎么办?
  • 修改现有标签是不可能的。
  • 我们不能重新 构建新的struct标签 。

为了改进这一点,我编写了一个自定义的Go包,它修复了上面的所有问题,并提供了一个可以轻松修改struct标签的每个方面的API。

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


  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.  
  6.     "github.com/fatih/structtag" 
  7.  
  8. func main() { 
  9.     tag := `json:"foo,omitempty,string" xml:"foo"
  10.  
  11.     // parse the tag 
  12.     tags, err := structtag.Parse(string(tag)) 
  13.     if err != nil { 
  14.         panic(err) 
  15.     } 
  16.  
  17.     // iterate over all tags 
  18.     for _, t := range tags.Tags() { 
  19.         fmt.Printf("tag: %+v\n", t) 
  20.     } 
  21.  
  22.     // get a single tag 
  23.     jsonTag, err := tags.Get("json"
  24.     if err != nil { 
  25.         panic(err) 
  26.     } 
  27.  
  28.     // change existing tag 
  29.     jsonTag.Name = "foo_bar" 
  30.     jsonTag.Options = nil 
  31.     tags.Set(jsonTag) 
  32.  
  33.     // add new tag 
  34.     tags.Set(&structtag.Tag{ 
  35.         Key:     "hcl"
  36.         Name:    "foo"
  37.         Options: []string{"squash"}, 
  38.     }) 
  39.  
  40.     // print the tags 
  41.     fmt.Println(tags) // Output: json:"foo_bar" xml:"foo" hcl:"foo,squash" 

既然我们已经知道如何解析一个struct标签了,以及修改它或创建一个新的,现在是时候来修改一个有效的Go源文件了。在上面的示例中,标签已经存在了,但是如何从现有的Go结构中获取标签呢?

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


(结构体的基本的Go ast.Node 表示)

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

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

要做到这一点,我们将使用 go/parser 包来 解析 文件以获取(整个文件的)AST,然后使用 go/ast 包来遍历整棵树(我们也可以手动执行, 但这是另一篇博文的主题)。下面代码你可以看到一个完整的例子:


  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.     "go/ast" 
  6.     "go/parser" 
  7.     "go/token" 
  8.  
  9. func main() { 
  10.     src := `package main 
  11.         type Example struct { 
  12.     Foo string` + " `json:\"foo\"` }" 
  13.  
  14.     fset := token.NewFileSet() 
  15.     file, err := parser.ParseFile(fset, "demo", src, parser.ParseComments) 
  16.     if err != nil { 
  17.         panic(err) 
  18.     } 
  19.  
  20.     ast.Inspect(file, func(x ast.Node) bool { 
  21.         s, ok := x.(*ast.StructType) 
  22.         if !ok { 
  23.             return true 
  24.         } 
  25.  
  26.         for _, field := range s.Fields.List { 
  27.             fmt.Printf("Field: %s\n", field.Names[0].Name
  28.             fmt.Printf("Tag:   %s\n", field.Tag.Value) 
  29.         } 
  30.         return false 
  31.     }) 

上面代码输出如下:


  1. Field: Foo  
  2. Tag: `json:"foo"

上面代码执行以下操作:

  • 我们定义了仅包含一个结构体的有效Go包的实例。
  • 我们使用 go/parser 包来解析这个字符串。解析器包也可以从磁盘读取文件(或整个包)。
  • 在我们解析之后,我们保存我们的节点(分配给变量文件)并查找由 *ast.StructType 定义的AST节点(参见AST映像作为参考)。遍历树是通过ast.Inspect()函数完成的。它会遍历所有节点,直到它收到false值。这是非常方便的,因为它不需要知道每个节点。
  • 我们打印结构体的字段名称和结构标签。

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

既然我们有了这些,我们可以通过使用这两个重要的代码片段开始构建我们的工具(名为 gomodifytags )。该工具应顺序执行以下操作:

  • 获取配置,以识别我们要修改哪个结构体
  • 根据配置查找和修改结构体
  • 输出结果

由于 gomodifytags 将主要由编辑器来执行,我们打算通过 CLI 标志传递配置信息。第二步包含多个步骤,如解析文件、找到正确的结构体,然后修改结构(通过修改 AST 完成)。最后,我们将输出结果,或是按照原始的 Go 源文件或是某种自定义协议(如 JSON,稍后再说)。

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

让我们开始详细解释每个步骤。为了保持简单,我将尝试以萃取形式解释重要的部分。尽管一切都是一样的,一旦你读完了这篇博文,你将能够在无需任何指导的情况下通读整个源代码(你将会在本指南的最后找到所有资源)

让我们从第一步开始,了解如何 获取配置 。以下是我们的配置文件,其中包含所有的必要信息


  1. type config struct { 
  2.     // first section - input & output 
  3.     file     string 
  4.     modified io.Reader 
  5.     output   string 
  6.     write    bool 
  7.  
  8.     // second section - struct selection 
  9.     offset     int 
  10.     structName string 
  11.     line       string 
  12.     start, end int 
  13.  
  14.     // third section - struct modification 
  15.     remove    []string 
  16.     add       []string 
  17.     override  bool 
  18.     transform string 
  19.     sort      bool 
  20.     clear     bool 
  21.     addOpts    []string 
  22.     removeOpts []string 
  23.     clearOpt   bool 

它分为 三个 主要部分:

第一部分包含有关如何和哪个文件要读入的配置。这可以是本地文件系统的文件名,也可以是直接来自stdin的数据(主要用在编辑器中)。它还设置了如何输出结果(Go源文件或JSON形式),以及我们是否应该覆写文件,而不是输出到stdout中。

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

而编辑器最好使用 字节偏移量 。例如下面你可以看到我们的光标刚好在“Port”字段名称之后,从那里我们可以很容易地得到起始行号:

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

我们该如何获得配置呢? 我们只需使用flag包,然后为配置中的每个字段创建一个标志,然后给他们赋值。举个例子:


  1. flagFile := flag.String("file""""Filename to be parsed"
  2. cfg := &config{ 
  3.     file: *flagFile, 

我们对 配置中的每个字段 执行相同操作。相关完整的列表请查看gomodifytag的当前master分支上的 flag 定义。

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


  1. func main() { 
  2.     cfg := config{ ... } 
  3.  
  4.     err := cfg.validate() 
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
go goroutine 怎样更好的进行错误处理发布时间:2022-07-10
下一篇:
go语言golang证书相关发布时间:2022-07-10
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap