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

自己事先编写过一个叫 gomodifytags
的工具,它使本人的生存变得很轻便。它会依照字段名称自动填写结构体标具名段。让作者来显示一下它的坚守:

json 1

利用那样的工具得以很轻易管理结构体的四个字段。该工具还足以增多和删除标签、管理标签选项(如
omitempty)、定义调换规则(snake_casecamelCase
等)等。但该工具是怎么工作的吧?它当中使用了怎么 Go
包?有为数不少主题材料须要应对。

那是一篇非常的短的博文,其解说了什么样编写那样的工具以及各类营造细节。它包蕴众多奇特的底细、本事和不解的
Go 知识。

拿起1杯咖啡☕️,让我们深远一下吧!


首先,让本身列出那几个工具要求做的业务:

  1. 它必要读取源文件、驾驭并能够分析 Go 文件
  2. 它须求找到相关的结构体
  3. 找到结构体后,它须要获得字段名称
  4. 它需求根据字段名来更新结构体标签(依照转变规则,如 snake_case
  5. 它需求能够把那几个更改更新到文件中,恐怕能够以可消费的形式出口改造后的结果

咱俩先是来打听怎么是
结构体(struct)标签(tag),从此处大家得以学学到具有东西以及怎样把它们构成在一块利用,在此基础上您能够创设出这么的工具。

json 2

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

json 3

以此定义有点长,不是很轻易令人知道。大家尝试分解一下它:

  • 一个结构体标签是三个字符串文字(因为它有字符串类型)
  • 键(key)部分是贰个无引号的字符串文字
  • 值(value)部分是带引号的字符串文字
  • 键和值由冒号(:)分隔。键与值且由冒号分隔组成的值称为键值对
  • 结构体标签能够带有多少个键值对(可选)。键值对由空格分隔
  • 不是概念的1部分是选用设置。像 encoding/json
    那样的包在读取值时作为1个由逗号分隔列表。
    第九个逗号后的内容都是选拔部分,比如
    foo,omitempty,string。其有三个名叫 foo 的值和 [omitempty,
    string] 选项
  • 因为结构体标签是字符串文字,所以须求利用双引号或反引号包围。因为值必须接纳引号,由此大家总是选用反引号对一切标签做拍卖。

因此看来:

json 4

大家曾经掌握了怎么是结构体标签,大家能够依据须求轻易地修改它。
今后的难点是,大家什么样分析它本事使大家能够轻易举办更换?幸运的是,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

借使键不存在,则赶回1个空字符串。

那是丰盛有用,也有一部分欠缺使得它并不适合大家,因为我们须求越多的油滑:

  • 它不能够检验到标签是或不是格式错误(如:键部分用引号包裹,值部分未有利用引号等)。
  • 它不能够获悉选项的语义
  • 它没有主意迭代现存的竹签或重返它们。我们务须要明白要修改哪些标签。假使不通晓名字如何做?
  • 修改现存标签是不可能的。
  • 我们不能从头初步创设新的结构体标签

为了精益求精那点,小编写了四个自定义的 Go
包,它化解了地方提到的富至极,并提供了一个API,能够轻松地转移结构体标签的各类方面。

json 5

该包名字为 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(简化版):

json 6

在那棵树中,大家能够查找和操作种种标志符、各种字符串、每种括号等。那么些都由
AST
节点
意味着。例如,大家能够透过轮换表示它的节点将字段名称从
Foo 更改为 Bar。 该逻辑同样适用于结构体标签。

得到贰个 Go AST,大家须求解析源文件并将其转变到二个AST。实际上,那四头都以通过同2个手续来处理的。

要兑现那一点,大家将应用 go/parser
包来解析文件以博得 AST(整个文件),然后利用
go/ast
包来拍卖任何树(我们得以手动做这么些工作,但那是另壹篇博文的核心)。
您在底下能够看到四个完全的例证:

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    src := `package main
        type Example struct {
    Foo string` + " `json:\"foo\"` }"

    fset := token.NewFileSet()
    file, err := parser.ParseFile(fset, "demo", src, parser.ParseComments)
    if err != nil {
        panic(err)
    }

    ast.Inspect(file, func(x ast.Node) bool {
        s, ok := x.(*ast.StructType)
        if !ok {
            return true
        }

        for _, field := range s.Fields.List {
            fmt.Printf("Field: %s\n", field.Names[0].Name)
            fmt.Printf("Tag:   %s\n", field.Tag.Value)
        }
        return false
    })
}

输出结果:

Field: Foo
Tag:   `json:"foo"`

代码实行以下操作:

  • 咱俩利用1个单身的结构体定义了2个 Go 包示例
  • 我们选拔 go/parser 包来分析这几个字符串。parser
    包也能够从磁盘读取文件(或任何包)。
  • 在条分缕析后,我们处理了节点(分配给变量文件)并找寻由
    ast.StructType 定义的
    AST 节点(参考 AST 图)。通过 ast.Inspect()
    函数完毕树的拍卖。它会遍历全数节点,直到它接受 false 值。
    那是充足有利于的,因为它不供给精通各类节点。
  • 我们打字与印刷了结构体的字段名称和布局体标签。

我们以后能够做两件首要的事,首先,大家精晓了怎么着分析三个 Go
源文件
并寻觅结构体标签(通过 go/parser)。其次,我们理解了怎么浅析
Go 结构体标签
,并依照须求张开改换(通过
github.com/fatih/structtag)。

咱俩有了这几个,将来能够通过运用那四个知识点早先创设大家的工具(命名叫
gomodifytags)。该工具应按顺序推行以下操作

  • 赢得配置,用于告诉大家要修改哪个结构体
  • 基于布置查找和修改结构体
  • 输出结果

由于 gomodifytags 将重大使用于编辑器,我们将经过 CLI
标识传入配置。第①步包蕴三个步骤,如解析文件,找到正确的结构体,然后修改结构体(通过退换AST)。最终,我们将结果输出,无论结果的格式是原来的 Go
源文件或然某种自定义商谈(如 JSON,稍后再说)。

以下是简化版 gomodifytags 的主要性意义:

json 7

让我们更详实地说明每二个手续。为了轻巧起见,笔者将尝试以囊括的花样来解释首要部分。
原理都同样,一旦您读完那篇博文,你将能够在一直不任何教导情状下阅整个源码(指南最后附带了全数能源)

让大家从第三步伊始,驾驭什么得到配置。以下是我们的布置,包蕴全数须求的消息

type config struct {
    // first section - input & output
    file     string
    modified io.Reader
    output   string
    write    bool

    // second section - struct selection
    offset     int
    structName string
    line       string
    start, end int

    // third section - struct modification
    remove    []string
    add       []string
    override  bool
    transform string
    sort      bool
    clear     bool
    addOpts    []string
    removeOpts []string
    clearOpt   bool
}

它分为三个首要部分:

第二部分含有关于怎么着读取和读取哪个文件的安装。那能够是本土文件系统的文件名,也得以一一向源于
stdin(首要用在编写器中)。 它还安装哪些输出结果(go 源文件或
JSON),以及是不是相应覆盖文件而不是出口到 stdout。

其次局地概念了什么样采纳二个结构体及其字段。有种种情势能够成功那或多或少。
我们能够透过它的舞狮(光标地点)、结构体名称、一行单行(仅选择字段)或一名目许多行来定义它。最终,大家无论怎么着都赢得开头行/结束行。例如在底下的例子中,您可以看看,大家选用它的名字来挑选结构体,然后提取开首行和截至行以接纳正确的字段:

json 8

假若是用于编辑器,则最棒应用字节偏移量。例如上面你能够窥见大家的光标刚辛亏
port 字段名称前边,从那边我们得以很轻易地获取伊始行/截止行:

json 9

安排中的其四个部分实质上是3个炫丽到 structtag
包的一对壹映射。它基本上允许我们在读取字段后将配置传给 structtag 包。
如你所知,structtag
包允许我们解析五个结构体标签并对一壹部分进行改变。但它不会覆盖或更新结构体字段。

大家怎么获得配置?我们只需利用 flag
包,然后为布局中的每一种字段创设三个标明,然后分配它们。举个例子:

flagFile := flag.String("file", "", "Filename to be parsed")
cfg := &config{
    file: *flagFile,
}

我们对安插中的各种字段试行同样操作。有关总体内容,请查看 gomodifytag
当前 master
分支的标明定义

比方我们有了配置,就足以做些基本的表达:

func main() {
    cfg := config{ ... }

    err := cfg.validate()
    if err != nil {
        log.Fatalln(err)
    }

    // continue parsing
}

// validate validates whether the config is valid or not
func (c *config) validate() error {
    if c.file == "" {
        return errors.New("no file is passed")
    }

    if c.line == "" && c.offset == 0 && c.structName == "" {
        return errors.New("-line, -offset or -struct is not passed")
    }

    if c.line != "" && c.offset != 0 ||
        c.line != "" && c.structName != "" ||
        c.offset != 0 && c.structName != "" {
        return errors.New("-line, -offset or -struct cannot be used together. pick one")
    }

    if (c.add == nil || len(c.add) == 0) &&
        (c.addOptions == nil || len(c.addOptions) == 0) &&
        !c.clear &&
        !c.clearOption &&
        (c.removeOptions == nil || len(c.removeOptions) == 0) &&
        (c.remove == nil || len(c.remove) == 0) {
        return errors.New("one of " +
            "[-add-tags, -add-options, -remove-tags, -remove-options, -clear-tags, -clear-options]" +
            " should be defined")
    }

    return nil
}

将表明部分放置在2个单独的函数中,以便测试。
近期大家询问了何等获取配置并拓展认证,我们后续分析文件:

json 10

大家曾经伊始商讨哪边分析文件了。那里的解析是 config
结构体的三个格局。实际上,全数的主意都以 config 结构体的一片段:

func main() {
    cfg := config{}

    node, err := cfg.parse()
    if err != nil {
        return err
    }

    // continue find struct selection ...
}

func (c *config) parse() (ast.Node, error) {
    c.fset = token.NewFileSet()
    var contents interface{}
    if c.modified != nil {
        archive, err := buildutil.ParseOverlayArchive(c.modified)
        if err != nil {
            return nil, fmt.Errorf("failed to parse -modified archive: %v", err)
        }
        fc, ok := archive[c.file]
        if !ok {
            return nil, fmt.Errorf("couldn't find %s in archive", c.file)
        }
        contents = fc
    }

    return parser.ParseFile(c.fset, c.file, contents, parser.ParseComments)
}

parse 函数只做一件事:解析源代码并回到叁个
ast.Node。假使我们传入的是文件,那就卓殊简单了,在那种状态下,我们采纳parser.ParseFile() 函数。必要注意的是 token.NewFileSet(),它创制1个
*token.FileSet 类型。大家将它存款和储蓄在 c.fset 中,同时也传给了
parser.ParseFile() 函数。为何吗?

因为 fileset
用于为各种文件独立储存各个节点的地方新闻。那在后来特别有用,能够用于获取
ast.Node 的确切地方(请留意,ast.Node 使用了贰个减去了的任务新闻
token.Pos。要赢得越多的音信,它必要通过 token.FileSet.Position()
函数来获得1个 token.Position,其包括越多的新闻)

让大家再三再四。要是通过 stdin 传递源文件,那么那进一步好玩。config.modified
字段是一个轻松测试的 io.Reader,但事实上大家传递的是
stdin。大家什么检验是不是要求从 stdin 读取呢?

咱俩理解用户是或不是想透过 stdin 传递内容。那种情景下,工具用户须求传递
--modified 标识(那是一个布尔申明)。假设用户了传递它,大家只需将
stdin 分配给 c.modified

flagModified = flag.Bool("modified", false,
    "read an archive of modified files from standard input")

if *flagModified {
    cfg.modified = os.Stdin
}

只要重复检查上面的 config.parse() 函数,您将发现大家检查是否已分配了
.modified 字段。因为 stdin
是2个自由的数据流,大家要求能够基于给定的商议举行剖析。在那种气象下,大家如果存档包蕴以下内容:

  • 文件名,后接一行新行
  • 文件大小(10进制),后接1行新行
  • 文件的剧情

因为大家精通文件大小,能够无障碍地剖析文件内容。任韩德明出给定文件大小的壹部分,大家唯有甘休解析。

方法也被别的多少个工具所选拔(如 gurugogetdoc
等),对编辑器来说11分管用。
因为那样能够让编辑器传递修改后的文书内容,而不会保留到文件系统中。因而命名称为
modified

现行我们有了上下一心的节点,让大家继续 “找出结构体” 这一步:

json 11

在 main 函数中,我们将选择从上一步解析获得的 ast.Node 调用
findSelection() 函数:

func main() {
    // ... parse file and get ast.Node

    start, end, err := cfg.findSelection(node)
    if err != nil {
        return err
    }

    // continue rewriting the node with the start&end position
}

cfg.findSelection()
函数基于配置重回结构体的开端地方和终结地方以报告我们什么样抉择贰个结构体。它迭代给定节点,然后重回初始地点/截至地点(如上安插部分中所述):

json 12

但是怎么办啊?记住有二种方式。分别是选择、偏移量结构体名称

// findSelection returns the start and end position of the fields that are
// suspect to change. It depends on the line, struct or offset selection.
func (c *config) findSelection(node ast.Node) (int, int, error) {
    if c.line != "" {
        return c.lineSelection(node)
    } else if c.offset != 0 {
        return c.offsetSelection(node)
    } else if c.structName != "" {
        return c.structSelection(node)
    } else {
        return 0, 0, errors.New("-line, -offset or -struct is not passed")
    }
}

分选是最简易的部分。那里大家只回去标记值本人。因而只要用户传入
--line 3,50 标志,函数将回到(3, 50, nil)
它所做的就是拆分标记值并将其改变为整数(同样实行验证):

func (c *config) lineSelection(file ast.Node) (int, int, error) {
    var err error
    splitted := strings.Split(c.line, ",")

    start, err := strconv.Atoi(splitted[0])
    if err != nil {
        return 0, 0, err
    }

    end := start
    if len(splitted) == 2 {
        end, err = strconv.Atoi(splitted[1])
        if err != nil {
            return 0, 0, err
        }
    }

    if start > end {
        return 0, 0, errors.New("wrong range. start line cannot be larger than end line")
    }

    return start, end, nil
}

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

偏移量结构体名称接纳须求做更加多的做事。
对于那几个,我们首先需求搜罗全数给定的结构体,以便能够估测计算偏移地方或索求结构体名称。为此,大家第2要有1个采访全体结构体的函数:

// collectStructs collects and maps structType nodes to their positions
func collectStructs(node ast.Node) map[token.Pos]*structType {
    structs := make(map[token.Pos]*structType, 0)
    collectStructs := func(n ast.Node) bool {
        t, ok := n.(*ast.TypeSpec)
        if !ok {
            return true
        }

        if t.Type == nil {
            return true
        }

        structName := t.Name.Name

        x, ok := t.Type.(*ast.StructType)
        if !ok {
            return true
        }

        structs[x.Pos()] = &structType{
            name: structName,
            node: x,
        }
        return true
    }
    ast.Inspect(node, collectStructs)
    return structs
}

我们应用 ast.Inspect() 函数稳步遍历 AST 并查找结构体。
大家率先寻找 *ast.TypeSpec,以便大家得以博得结构体名称。搜索
*ast.StructType 时给定的是结构体本身,而不是它的名字。
那正是干什么大家有1个自定义的 structType
类型,它保存了名称和组织体节点本身。那样在壹1地点都很有益于。
因为每一个结构体的地方都以唯一的,并且在同样职位上不也许存在多个不等的结构体,由此大家利用地点作为
map 的键。

近来我们全部了全部结构体,在最终能够回去二个结构体的前奏地点和告竣地点的偏移量和结构体名称格局。
对于偏移地点,大家检查偏移是不是在加以的结构体之间:

func (c *config) offsetSelection(file ast.Node) (int, int, error) {
    structs := collectStructs(file)

    var encStruct *ast.StructType
    for _, st := range structs {
        structBegin := c.fset.Position(st.node.Pos()).Offset
        structEnd := c.fset.Position(st.node.End()).Offset

        if structBegin <= c.offset && c.offset <= structEnd {
            encStruct = st.node
            break
        }
    }

    if encStruct == nil {
        return 0, 0, errors.New("offset is not inside a struct")
    }

    // offset mode selects all fields
    start := c.fset.Position(encStruct.Pos()).Line
    end := c.fset.Position(encStruct.End()).Line

    return start, end, nil
}

咱俩应用 collectStructs()
来搜罗全体结构体,之后在此处迭代。还得记得大家存款和储蓄了用来解析文件的开端
token.FileSet 么?

最近得以用它来获取每一个协会体节点的舞狮音信(大家将其解码为1个
token.Position,它为我们提供了 .Offset 字段)。
大家所做的只是一个简便的检讨和迭代,直到大家找到结构体(那里命名字为
encStruct):

for _, st := range structs {
    structBegin := c.fset.Position(st.node.Pos()).Offset
    structEnd := c.fset.Position(st.node.End()).Offset

    if structBegin <= c.offset && c.offset <= structEnd {
        encStruct = st.node
        break
    }
}

有了这么些新闻,大家能够提取找到的结构体的始发地点和终止地点:

start := c.fset.Position(encStruct.Pos()).Line
end := c.fset.Position(encStruct.End()).Line

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

func (c *config) structSelection(file ast.Node) (int, int, error) {
    // ...

    for _, st := range structs {
        if st.name == c.structName {
            encStruct = st.node
        }
    }

    // ...
}

目前我们有了开头地方和了结地方,我们总算得以拓展第三步了:修改结构体字段。

json 13

main 函数中,大家将动用从上一步解析的节点来调用 cfg.rewrite()
函数:

func main() {
    // ... find start and end position of the struct to be modified


    rewrittenNode, errs := cfg.rewrite(node, start, end)
    if errs != nil {
        if _, ok := errs.(*rewriteErrors); !ok {
            return errs
        }
    }


    // continue outputting the rewritten node
}

那是该工具的主导。在 rewrite
函数中,大家将重写起来地方和了结地点之间的具备结构体字段。
在深深领悟从前,我们能够看一下该函数的大致内容:

// rewrite rewrites the node for structs between the start and end
// positions and returns the rewritten node
func (c *config) rewrite(node ast.Node, start, end int) (ast.Node, error) {
    errs := &rewriteErrors{errs: make([]error, 0)}

    rewriteFunc := func(n ast.Node) bool {
        // rewrite the node ...
    }

    if len(errs.errs) == 0 {
        return node, nil
    }

    ast.Inspect(node, rewriteFunc)
    return node, errs
}

正如您所看到的,大家重新行使 ast.Inspect()
来慢慢处理给定节点的树。大家重写 rewriteFunc
函数中的每一种字段的竹签(更加多内容在后头)。

因为传递给 ast.Inspect()
的函数不会再次回到错误,因而大家将开创1个谬误映射(使用 errs
变量定义),之后在大家稳步遍历树并处理种种独立的字段时募集错误。以往让大家来谈谈
rewriteFunc 的当中原理:

rewriteFunc := func(n ast.Node) bool {
    x, ok := n.(*ast.StructType)
    if !ok {
        return true
    }

    for _, f := range x.Fields.List {
        line := c.fset.Position(f.Pos()).Line

        if !(start <= line && line <= end) {
            continue
        }

        if f.Tag == nil {
            f.Tag = &ast.BasicLit{}
        }

        fieldName := ""
        if len(f.Names) != 0 {
            fieldName = f.Names[0].Name
        }

        // anonymous field
        if f.Names == nil {
            ident, ok := f.Type.(*ast.Ident)
            if !ok {
                continue
            }

            fieldName = ident.Name
        }

        res, err := c.process(fieldName, f.Tag.Value)
        if err != nil {
            errs.Append(fmt.Errorf("%s:%d:%d:%s",
                c.fset.Position(f.Pos()).Filename,
                c.fset.Position(f.Pos()).Line,
                c.fset.Position(f.Pos()).Column,
                err))
            continue
        }

        f.Tag.Value = res
    }

    return true
}

记住,AST 树中的每1个节点都会调用那一个函数。因而,大家只找出类型为
*ast.StructType 的节点。1旦大家具有,就能够初叶迭代结构体字段。

那边大家选取 startend
变量。那定义了我们是或不是要修改该字段。即使字段地方放在 start-end
之间,我们将继续,不然大家将忽略:

if !(start <= line && line <= end) {
    continue // skip processing the field
}

接下去,大家检查是不是留存标签。假设标署名段为空(也等于nil),则初始化标具名段。那在力促前边的 cfg.process() 函数幸免panic:

if f.Tag == nil {
    f.Tag = &ast.BasicLit{}
}

于今让作者先解释一下二个有趣的地点,然后再持续。gomodifytags
尝试获得字段的字段名称并拍卖它。但是,当它是2个匿名字段呢?:

type Bar string

type Foo struct {
    Bar //this is an anonymous field
}

在那种状态下,因为未有字段名称,大家品尝从品类名称中获取字段名称

// if there is a field name use it
fieldName := ""
if len(f.Names) != 0 {
    fieldName = f.Names[0].Name
}

// if there is no field name, get it from type's name
if f.Names == nil {
    ident, ok := f.Type.(*ast.Ident)
    if !ok {
        continue
    }

    fieldName = ident.Name
}

若果我们赢得了字段名称和标签值,就能够起来拍卖该字段。cfg.process()
函数负责处理有字段名称和标签值(假设有的话)的字段。在它回随处理结果后(在大家的例子中是
struct tag 格式),大家应用它来覆盖现成的标签值:

res, err := c.process(fieldName, f.Tag.Value)
if err != nil {
    errs.Append(fmt.Errorf("%s:%d:%d:%s",
        c.fset.Position(f.Pos()).Filename,
        c.fset.Position(f.Pos()).Line,
        c.fset.Position(f.Pos()).Column,
        err))
    continue
}

// rewrite the field with the new result,i.e: json:"foo"
f.Tag.Value = res

实质上,纵然你记得 structtag,它回到标签实例的 String()
表述。在大家回去标签的结尾表述此前,大家依照需求运用 structtag
包的种种方法修改结构体。以下是2个简短的认证图示:

json 14

譬如,大家要增加 process() 中的 removeTags()
函数。此意义接纳以下配置来创设要去除的标签数组(键名称):

flagRemoveTags = flag.String("remove-tags", "", "Remove tags for the comma separated list of keys")

if *flagRemoveTags != "" {
    cfg.remove = strings.Split(*flagRemoveTags, ",")
}

removeTags() 中,大家检查是不是采用了
--remove-tags。假设有,大家将采用 structtag 的
tags.Delete()
方法来删除标签:

func (c *config) removeTags(tags *structtag.Tags) *structtag.Tags {
    if c.remove == nil || len(c.remove) == 0 {
        return tags
    }

    tags.Delete(c.remove...)
    return tags
}

此逻辑同样适用于 cfg.Process() 中的全体函数。


大家早已有了2个重写的节点,让我们来谈谈最后八个话题。输出和格式化结果:

json 15

在 main 函数中,大家将动用上一步重写的节点来调用 cfg.format() 函数:

func main() {
    // ... rewrite the node

    out, err := cfg.format(rewrittenNode, errs)
    if err != nil {
        return err
    }

    fmt.Println(out)
}

您需求小心的壹件事是,大家输出到
stdout。那佯做有过多独到之处。首先,您只需运营工具就能查看到结果,
它不会转移任何事物,只是为着让工具用户及时看到结果。其次,stdout
是可构成的,能够重定向到此外市方,甚至足以用来覆盖原来的工具。

近来我们来探视 format() 函数:

func (c *config) format(file ast.Node, rwErrs error) (string, error) {
    switch c.output {
    case "source":
        // return Go source code
    case "json":
        // return a custom JSON output
    default:
        return "", fmt.Errorf("unknown output mode: %s", c.output)
    }
}

我们有两种输出情势

第一个source)以 Go 格式打字与印刷
ast.Node。那是默许选项,假如你在命令行使用它或只想看到文件中的退换,那么那格外适合您。

第二个选项(json)更为先进,其专为别的环境而设计(尤其是编辑器)。它依照以下结构体对出口举行编码:

type output struct {
    Start  int      `json:"start"`
    End    int      `json:"end"`
    Lines  []string `json:"lines"`
    Errors []string `json:"errors,omitempty"`
}

对工具进行输入和结尾结出输出(未有其余不当)大约示意图如下:

json 16

回到 format() 函数。如以前所述,有二种方式。source 形式选择
go/format 包将 AST 格式化为 Go
源码。该软件包也被众多别样合法工具(如 gofmt)使用。以下是
source 情势的兑现格局:

var buf bytes.Buffer
err := format.Node(&buf, c.fset, file)
if err != nil {
    return "", err
}

if c.write {
    err = ioutil.WriteFile(c.file, buf.Bytes(), 0)
    if err != nil {
        return "", err
    }
}

return buf.String(), nil

格式包接受 io.Writer
并对其举办格式化。那就是干吗大家创立一个中间缓冲区(var buf bytes.Buffer)的由来,当用户传入四个
-write
标记时,大家得以采取它来覆盖文件。格式化后,我们再次回到缓冲区的字符串表示情势,个中蕴藏格式化后的
Go 源代码。

json
方式更有趣。因为大家回来的是1段源代码,由此我们须求规范地展现它原先的格式,那也表示要把注释包涵进去。难题在于,当使用
format.Node() 打字与印刷单个结构体时,假设它们是有损的,则不能够打字与印刷出 Go
注释。

什么样是有损注释(lossy comment)?看看这么些事例:

type example struct {
    foo int 

    // this is a lossy comment

    bar int 
}

种种字段都以 *ast.Field 类型。此结构体有三个 *ast.Field.Comment
字段,其含有某字段的评释。

不过,在上头的例子中,它属于什么人?属于 foo 还是 bar

因为不可能分明,那么些注释被称呼有损注释。假如未来利用 format.Node()
函数打字与印刷上边包车型地铁结构体,就会并发难题。
当你打字与印刷它时,你或者会获得(https://play.golang.org/p/peHsswF4JQ):

type example struct {
    foo int

    bar int
}

主题素材在于有损注释是 *ast.File
一部分它与树分开。只有打字与印刷整个文件时能力打字与印刷出来。
所以消除办法是打字与印刷整个文件,然后删除掉我们要在 JSON 输出中回到的内定行:

var buf bytes.Buffer
err := format.Node(&buf, c.fset, file)
if err != nil {
    return "", err
}

var lines []string
scanner := bufio.NewScanner(bytes.NewBufferString(buf.String()))
for scanner.Scan() {
    lines = append(lines, scanner.Text())
}

if c.start > len(lines) {
    return "", errors.New("line selection is invalid")
}

out := &output{
    Start: c.start,
    End:   c.end,
    Lines: lines[c.start-1 : c.end], // cut out lines
}

o, err := json.MarshalIndent(out, "", "  ")
if err != nil {
    return "", err
}

return string(o), nil

这么做保险大家能够打字与印刷全数注释。


那就是全体内容!

小编们成功做到了我们的工具,以下是大家在漫天指南开中学施行的全部步骤图:

json 17

追忆一下大家做了什么样:

  • 咱们经过 CLI 标识检索配置
  • 咱俩因此 go/parser 包解析文件来博取3个 ast.Node
  • 在分析文件从此,大家搜索
    获取相应的结构体来得到起第几人置和终结地方,那样我们得以清楚须要修改哪些字段
  • 若是大家有了开端地点和了结地点,我们再一次遍历
    ast.Node,重写起来地方和截止地方之间的种种字段(通过接纳
    structtag 包)
  • 此后,我们将格式化重写的节点,为编辑器输出 Go 源代码或自定义的 JSON

在创制此工具后,作者接到了诸多投机的评介,评论者们提到了那一个工具怎么着简化他们的家常工作。正如你所见到,固然看起来它很轻便制作,但在漫天指南中,我们已经指向广大新鲜的情形做了专门处理。

gomodifytags
成功应用于以下编辑器和插件已经有多少个月了,使得数以千计的开荒职员提高了工效:

  • vim-go
  • atom
  • vscode
  • acme

万一您对原始源代码感兴趣,能够在此地找到:

我还在 json,Gophercon 2017 上发表了五个发言,如若您感兴趣,可点击上边包车型大巴youtube 截图来看:

json 18

谢谢您读书此文。希望以此指南能诱发您从头成立八个新的 Go 工具。

相关文章

网站地图xml地图