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

自身前编写了一个叫 gomodifytags
的家伙,它一旦我的活着变得非常自在。它会依据字段名称自动填写结构体标签字段。让自己来显示一下她的效益:

图片 1

使这样的工具得以挺轻管理结构体的差不多单字段。该工具还得长和去标签、管理标签选项(如
omitempty)、定义转换规则(snake_casecamelCase
等)等。但拖欠工具是何许工作之为?它其中用了哟 Go
包?有众多题目亟需报。

及时是同等首十分丰富的博文,其说了安编写这样的工具和每个构建细节。它含众多奇之底细、技巧及未知的
Go 知识。

将起一杯咖啡☕️,让咱深刻一下吧!


率先,让自身列出这个家伙要举行的事务:

  1. 它需要读取源文件、理解并能够分析 Go 文件
  2. 其要找到相关的结构体
  3. 找到结构体后,它用取得字段名称
  4. 她需依据字段名来更新结构体标签(根据转换规则,如 snake_case
  5. 它们用能够将这些改动更新到文件被,或者会以可消费的章程出口更改后的结果

咱们先是来打听什么是
结构体(struct)标签(tag),从此间我们可以上及持有东西以及哪些拿它组合在一起使用,在斯基础及您得构建出如此的工具。

图片 2

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

图片 3

其一概念有硌长,不是异常轻受丁知晓。我们品尝分解一下它:

  • 一个结构体标签是一个字符串文字(因为其发字符串类型)
  • 键(key)部分是一个无引号的字符串文字
  • 值(value)部分是带引号的字符串文字
  • 键和值由冒号(:)分隔。键和值且由冒号分隔成的值称为键值对
  • 结构体标签可以含有多只键值对(可选)。键值对是因为空格分隔
  • 匪是概念之有的是挑设置。像 encoding/json
    这样的保在宣读取值时作为一个是因为逗号分隔列表。
    第一单逗号后底情节都是选项部分,比如
    foo,omitempty,string。其产生一个号称也 foo 的值和 [omitempty,
    string] 选项
  • 以结构体标签是字符串文字,所以用采用对引号或反引号包围。因为价值必须下引号,因此我们连用反引号对全体标签做拍卖。

如上所述:

图片 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

要键不设有,则赶回一个空字符串。

立即是殊有因此,否产生局部不足教其并无称我们,因为咱们要重多的八面玲珑:

  • 它们无法检测及标签是否格式错误(如:键部分用引号包裹,值部分无应用引号等)。
  • 它无法获知选项的语义
  • 它没有方法迭代现有的标签或回到其。我们要使懂如果修改哪些标签。如果非清楚名字怎么惩罚?
  • 修改现有标签是免可能的。
  • 我们不能从头开始构建新的结构体标签

为精益求精就或多或少,我形容了一个自定义的 Go
包,它解决了上面提到的拥有问题,并提供了一个
API,可以轻松地改变结构体标签的各个方面。

图片 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(简化版):

图片 6

每当及时株树中,我们可以找寻和操作每个标识符、每个字符串、每个括号等。这些都出于
AST
节点代表。例如,我们可由此轮换表示它的节点将字段名称从
Foo 更改为 Bar。 该逻辑同样适用于结构体标签。

赢得一个 Go AST,我们需要解析源文件并将那易成为一个
AST。实际上,这两者都是透过跟一个步骤来处理的。

倘若实现即或多或少,我们将运 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"`

代码执行以下操作:

  • 咱用一个独立的布局体定义了一个 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 的要紧意义:

图片 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。

第二有概念了安选一个结构体及其字段。有多方法可以形成就或多或少。
我们可由此她的撼动(光标位置)、结构体名称、一行单行(仅选择字段)或同等名目繁多行来定义其。最后,我们无论如何都赢得初步行/结束行。例如在下面的事例中,您得看出,我们用它的讳来抉择结构体,然后取开始实行与了结行因为挑选对的字段:

图片 8

要是是用于编辑器,则最好应用字节偏移量。例如下面你可发现我们的光标刚好在
port 字段名称后,从那边我们得以老爱地得初步行/结束行:

图片 9

配置中之老三单部分实际上是一个照到 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
}

以证明部分放置于一个单独的函数中,以便测试。
今日咱们了解了怎样获取配置并进行说明,我们延续分析文件:

图片 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(),它创建一个
*token.FileSet 类型。我们以它存储在 c.fset 中,同时也传于了
parser.ParseFile() 函数。为什么吧?

因为 fileset
用于为每个文件独立积存每个节点的位置信息。这在后好有因此,可以用来取
ast.Node 的确切位置(请小心,ast.Node 使用了一个减了的位置信息
token.Pos。要拿走更多之音讯,它需要经 token.FileSet.Position()
函数来博一个 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
是一个随机的数据流,我们用能够根据加的协商进行解析。在这种景象下,我们要存档包含以下内容:

  • 文本称,后接一行新行
  • 文件大小(十进制),后搭一行新行
  • 文本之始末

因咱们解文件大小,可以随便障碍地解析文件内容。任何超出给定文件大小的一些,我们才停止解析。

方法呢叫其他几只器所采用(如 gurugogetdoc
等),对编辑器来说很有效。
因为这样可让编辑器传递修改后底文书内容,而免见面保留至文件系统中。因此命名也
modified

如今我们出矣温馨之节点,让咱继续 “搜索结构体” 这同一步:

图片 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()
函数冲配置返回结构体的开始位置和终止位置为告我们怎么选择一个结构体。它迭代给定节点,然后回开始位置/结束位置(如齐安排部分受到所陈述):

图片 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
}

当你当选一组行并高亮它们常,编辑器将使用这模式。

偏移量结构体名称摘用开还多之干活。
对于这些,我们率先用募所有给定的结构体,以便可以算偏移位置还是探寻结构体名称。为是,我们首先要来一个募集所有结构体的函数:

// 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 时给定的是结构体本身,而无是它的名。
这便是怎咱们发一个自定义的 structType
类型,它保存了号与结构体节点本身。这样在逐一地方还深便宜。
因为每个结构体的职位都是绝无仅有的,并且在相同职位上不可能在个别只不等之结构体,因此我们用位置作
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 么?

今昔好就此它们来得到每个组织体节点的撼动信息(我们以那解码为一个
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
        }
    }

    // ...
}

今我们发出矣起来位置以及竣工位置,我们好不容易可以进行第三步了:修改结构体字段。

图片 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()
的函数不会见回错误,因此我们拿创设一个荒谬映射(使用 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 树中的各级一个节点都见面调用这个函数。因此,我们唯有摸类型也
*ast.StructType 的节点。一旦我们具有,就可以开始迭代结构体字段。

此处我们应用 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
尝试获得字段的字段名称并处理它。然而,当其是一个匿名字段呢?:

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
包的各种措施修改结构体。以下是一个略的证实图示:

图片 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() 中的保有函数。


咱俩已起了一个重复写的节点,让咱们来讨论最后一个话题。输出及格式化结果:

图片 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"`
}

针对工具进行输入和结尾结出输出(没有其它不当)大概示意图如下:

图片 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
模式更有意思。因为我们回去的是同段子源代码,因此我们需要规范地展现她原先的格式,这也意味着一旦管注释包含进去。问题在于,当用
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

然做保险我们得打印所有注释。


眼看便是全部内容!

我们成功完成了我们的家伙,以下是咱于合指南中履行的完好步骤图:

图片 17

回首一下我们举行了什么:

  • 咱俩经过 CLI 标志检索配置
  • 我们经过 go/parser 包解析文件来收获一个 ast.Node
  • 当条分缕析文件后,我们搜索
    获取相应的结构体来博开始位置及了位置,这样我们得以知晓得改哪些字段
  • 而我们来了开位置与终止位置,我们又遍历
    ast.Node,重写起位置以及竣工位置之间的每个字段(通过使用
    structtag 包)
  • 下,我们拿格式化重写的节点,为编辑器输出 Go 源代码或于定义之 JSON

当创建是工具后,我接受了诸多投机的品,评论者们关系了这个家伙如何简化他们之日常工作。正而你所见到,尽管看起她非常轻做,但以全方位指南中,我们早已对广大新鲜之景举行了特别处理。

gomodifytags
成功应用为以下编辑器和插件都发出几乎只月了,使得数以千计的开发人员提升了工作效率:

  • vim-go
  • atom
  • vscode
  • acme

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

  • https://github.com/fatih/gomodifytags

我还在 Gophercon 2017 上刊出了一个演讲,如果您感兴趣,可点击下面的
youtube 截图来看:

图片 18

多谢君读书此文。希望以此指南能诱发而从头创建一个初的 Go 工具。

相关文章

网站地图xml地图