深入理解 Go 语言中的 Testable Examples

栏目: Go · 发布时间: 5年前

内容简介:隐藏的2016 年 10 月 10 日Golang 的工具链实现了名为

隐藏的 astparser 包的介绍

2016 年 10 月 10 日

Golang 的 工具 链实现了名为 Testable Examples 的功能。如果对该功能没有什么印象的话,我强烈建议首先阅读 “ Testable Examples in Go ” 博文进行了解。通过这篇文章我们将了解到该功能的整个解决方案以及如何构建其简化版本。

让我们看看 Testable Examples 的工作原理:

upper_test.go:

package main
import (
    "fmt"
    "strings"
)
func ExampleToUpperOK() {
    fmt.Println(strings.ToUpper("foo"))
    // Output: FOO
}
func ExampleToUpperFail() {
   fmt.Println(strings.ToUpper("bar"))
   // Output: BAr
}
> Go test -v
=== RUN   ExampleToUpperOK
--- PASS: ExampleToUpperOK (0.00s)
=== RUN   ExampleToUpperFail
--- FAIL: ExampleToUpperFail (0.00s)
got:
BAR
want:
BAr
FAIL
exit status 1
FAIL    Github.com/mlowicki/sandbox     0.008s

与测试函数一样的 Examples 放在 xxx_test.go 文件中,但前缀为 Example 而不是 Testgo test 命令使用特殊格式的注释( Output:something )并将它们与捕获的数据进行比较,通常写入 stdout 。其他工具(例如 godoc )使用相同的注释来丰富自动生成的文档。

问题是 go testgodoc 如何从特殊注释中提取数据?语言中是否有任何秘密机制使其成为可能?或者也许一切都可以用众所周知的结构来实现?

事实证明,标准库提供了与 Go 本身解析源代码相关的元素(分布在几个包中)。这些工具生成抽象语法树并提供访问特殊注释的途径。

抽象语法树(AST)

AST 是解析时在源代码中找到的元素的树形表示。让我们考虑一个简单的表达式:

9 /(2 + 1)

可以使用代码段生成 AST

expr, err := parser.ParseExpr("9 / (2 + 1)")
if err != nil {
    log.Fatal(err)
}
ast.Print(nil, expr)

输出:

0 *ast.BinaryExpr {
1 . X: *ast.BasicLit {
2 . . ValuePos: 1
3 . . Kind: INT
4 . . Value: "9"
5 . }
6 . OpPos: 3
7 . Op: /
8 . Y: *ast.ParenExpr {
9 . . Lparen: 5
10 . . X: *ast.BinaryExpr {
11 . . . X: *ast.BasicLit {
12 . . . . ValuePos: 6
13 . . . . Kind: INT
14 . . . . Value: "2"
15 . . . }
16 . . . OpPos: 8
17 . . . Op: +
18 . . . Y: *ast.BasicLit {
19 . . . . ValuePos: 10
20 . . . . Kind: INT
21 . . . . Value: "1"
22 . . . }
23 . . }
24 . . Rparen: 11
25 . }
26 }

使用图表可以简化输出,其中树形结构更明显:

(operator: /)
        /             \
       /               \
  (integer: 9) (parenthesized expression)
                        |
                        |
                  (operator: +)
                 /             \
                /               \
          (integer: 2)      (integer: 1)

使用 AST 时,两个标准包是至关重要的:

  • parser 提供用于解析用 Go 编写的源代码的结构
  • ast 实现用于在 Go 中使用 AST 代码的原始结构

通常在 词法分析 期间会删除注释。有一个特殊的标志来保存注释并将它们放入 AST -  parser.ParseComments

import (
    "fmt"
    "go/parser"
    "go/token"
    "log"
)
func main() {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "t.go", nil, parser.ParseComments)
    if err != nil {
        log.Fatal(err)
    }
    for _, group := range f.Comments {
        fmt.Printf("Comment group %#v\n", group)
        for _, comment := range group.List {
            fmt.Printf("Comment %#v\n", comment)
        }
    }
}

parser.ParseFile 的第三个参数是传递给 f.ex 的可选参数 , 类型可以是 stringio.Reader 。由于我使用了磁盘中的文件,因此设置为 nil

t.go:

package main
import "fmt"
// a
// b
func main() {
    // c
    fmt.Println("boom!")
}

输出:

Comment group &ast.CommentGroup{List:[]*ast.Comment{(*ast.Comment)(0x820262220), (*ast.Comment)(0x820262240)}}
Comment &ast.Comment{Slash:29, Text:"// a"}
Comment &ast.Comment{Slash:34, Text:"// b"}
Comment group &ast.CommentGroup{List:[]*ast.Comment{(*ast.Comment)(0x8202622c0)}}
Comment &ast.Comment{Slash:55, Text:"// c"}

Comment group

指的是一系列注释,中间没有任何元素。在上面的示例中,注释 “ a ”“ b ” 属于同一组。

Pos & Position

源代码中元素的位置使用 Pos 类型记录(其更详细的对应点是 Position )。它是一个单一的整数值,它对像 linecolumn 这样的信息进行编码,但 Position struct 将它们保存在不同的字段中。在外循环添加:

fmt.Printf("Position %#v\n", fset.PositionFor(group.Pos(), true))

程序额外输出:

Position token.Position{Filename:"t.go", Offset:28, Line:5, Column:1}
Position token.Position{Filename:"t.go", Offset:54, Line:9, Column:2}

Fileset

位置相对于解析文件集计算。每个文件都分配了不相交的范围,每个位置都位于其中一个范围内。在我们的例子中,我们只有一个,但需要整个集合来解码 Pos

fset.PositionFor(group.Pos(), true)

树遍历

ast 为深度优先遍历 AST 提供了方便的功能:

ast.Inspect(f, func(n ast.Node) bool {
    if n != nil {
        fmt.Println(n)
    }
    return true
})

由于我们知道如何提取所有注释,现在是时候找到所有顶级的 ExampleXXX 函数了。

doc.Examples

doc 提供了完全符合我们需要的功能:

package main
import (
    "fmt"
    "go/doc"
    "go/parser"
    "go/token"
    "log"
)
func main() {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "e.go", nil, parser.ParseComments)
    if err != nil {
        log.Fatal(err)
    }
    examples := doc.Examples(f)
    for _, example := range examples {
        fmt.Println(example.Name)
    }
}

e.go:

package main
import "fmt"
func ExampleSuccess() {
    fmt.Println("foo")
    // Output: foo
}
func ExampleFail() {
    fmt.Println("foo")
    // Output: bar
}

输出:

Fail
Success

doc.Examples 没有任何魔法技能。它依赖于我们已经看到的内容,主要是构建和遍历抽象语法树。让我们建立类似的东西:

package main
import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
    "log"
    "strings"
)
func findExampleOutput(block *ast.BlockStmt, comments []*ast.CommentGroup) (string, bool) {
    var last *ast.CommentGroup
    for _, group := range comments {
        if (block.Pos() < group.Pos()) && (block.End() > group.End()) {
            last = group
        }
    }
    if last != nil {
        text := last.Text()
        marker := "Output: "
        if strings.HasPrefix(text, marker) {
          return strings.TrimRight(text[len(marker):], "\n"), true
        }
    }
    return "", false
}
func isExample(fdecl *ast.FuncDecl) bool {
    return strings.HasPrefix(fdecl.Name.Name, "Example")
}
func main() {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "e.go", nil, parser.ParseComments)
    if err != nil {
        log.Fatal(err)
    }
    for _, decl := range f.Decls {
        fdecl, ok := decl.(*ast.FuncDecl)
        if !ok {
            continue
        }
        if isExample(fdecl) {
            output, found := findExampleOutput(fdecl.Body, f.Comments)
            if found {
                fmt.Printf("%s needs output '%s' \n", fdecl.Name.Name, output)
            }
        }
    }
}

输出:

ExampleSuccess needs output ‘foo’
ExampleFail needs output 'bar'

注释不是 AST 树的常规节点。它们可以通过 ast.File Comments 字段访问(由 f.ex. parser.ParseFile 返回)。此列表中的注释顺序与它们在源代码中显示的顺序相同。要查找某些块内的注释,我们需要比较上面的 findExampleOutput 中的位置:

var last *ast.CommentGroup
for _, group := range comments {
    if (block.Pos() < group.Pos()) && (block.End() > group.End()) {
        last = group
    }
}

if 语句中的条件检查 comment group 是否属于块的范围。

正如我们所看到的那样,标准库在解析时提供了很大的支持。那里的公共类库使整个工作非常愉快,并且精心设计的代码非常紧凑。

如果你喜欢这个帖子并希望获得有关新帖子的更新,请关注我。点击下面的❤,帮助他人发现这些资料。

相关资料


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

学习JavaScript数据结构与算法(第2版)

学习JavaScript数据结构与算法(第2版)

[巴西] Loiane Groner / 邓 钢、孙晓博、吴 双、陈 迪、袁 源 / 人民邮电出版社 / 2017-9 / 49.00元

本书首先介绍了JavaScript 语言的基础知识以及ES6 和ES7 中引入的新功能,接下来讨论了数组、栈、队列、链表、集合、字典、散列表、树、图等数据结构,之后探讨了各种排序和搜索算法,包括冒泡排序、选择排序、插入排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序、顺序搜索、二分搜索,然后介绍了动态规划和贪心算法等常用的高级算法以及函数式编程,最后还介绍了如何计算算法的复杂度。一起来看看 《学习JavaScript数据结构与算法(第2版)》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器