使用go ast和自动化释放Go代码的潜力
联系作者@:微信公众号 ,Medium ,LinkedIn ,Twitter
最近了解到了 stringer
的使用,顺便接触到 golang ast
的相关内容,才发现自己也可以写一些 Go 相关的自动化工具。
1.1 stringer and go generate 开发中经常有定义错误码这样的需求,错误码唯一标识具体的错误信息。另外还需要设置每个错误的具体描述。例如在 HTTP 协议中,200 表示 “OK”,404 表示”Not Found”。
传统方式下,每次定义错误码的时候,同时需要添加描述信息,而且描述信息经常会忘,不利于维护。但是我们可以通过 go generate
+ stringer
工具链优雅地解决这个问题。
stringer 的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Usage of stringer: stringer [flags] -type T [directory] stringer [flags] -type T files... For more information, see: https://pkg.go.dev/golang.org/x/tools/cmd/stringer Flags: -linecomment use line comment text as printed text when present -output string output file name; default srcdir/<type >_string.go -tags string comma-separated list of build tags to apply -trimprefix prefix trim the prefix from the generated constant names -type string comma-separated list of type names; must be set
举个例子:
1 2 3 4 5 6 7 8 9 10 package maintype ErrCode int const ( ERR_CODE_OK ErrCode = 0 ERR_CODE_INVALID_PARAMS ErrCode = 1 ERR_CODE_TIMEOUT ErrCode = 2 )
运行 go generate 生成如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "strconv" func _() { var x [1 ]struct {} _ = x[ERR_CODE_OK-0 ] _ = x[ERR_CODE_INVALID_PARAMS-1 ] _ = x[ERR_CODE_TIMEOUT-2 ] } const _ErrCode_name = "_OK_INVALID_PARAMS_TIMEOUT" var _ErrCode_index = [...]uint8 {0 , 3 , 18 , 26 }func (i ErrCode) String() string { if i < 0 || i >= ErrCode(len (_ErrCode_index)-1 ) { return "ErrCode(" + strconv.FormatInt(int64 (i), 10 ) + ")" } return _ErrCode_name[_ErrCode_index[i]:_ErrCode_index[i+1 ]] }
go generate 使用可以先阅读文章 Generating code - The Go Programming Language ,并运行 go help generate 查看基本用法。
1.2 stringer 是如何实现自动化生成的? 源代码实现:stringer.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import ( "bytes" "flag" "fmt" "go/ast" "go/constant" "go/format" "go/token" "go/types" "log" "os" "path/filepath" "sort" "strings" "golang.org/x/tools/go/packages" )
主要是”flag” 和 “go/ast”、”go/token”。”flag”主要处理一些命令行参数,可以参考我的一篇文章:Flag Package You should Know in Golang | by Wesley Wei | Programmer’s Career 。重点要理解 “go/ast”,”go/ast” 是 Go 语言处理源代码的重要工具,它不仅简化了代码分析的过程,还为代码生成和重构提供了强大的支持。下面我们来一点点介绍。
1.3 抽象语法树(AST) 学过编译原理的人可能听说过编译器的前端和后端,编译器的前端一般承担着词法分析、语法分析、类型检查和中间代码生成几部分工作,而编译器后端主要负责目标代码的生成和优化,也就是将中间代码翻译成目标机器能够运行的二进制机器码。
不搞编译器的我们大多只需要懂前端部分就行,不涉及后端,同时 go 官方还提供了开箱即用的库 go/ast
。
1.3.1 概念及其作用 抽象语法树(Abstract Syntax Tree,简称 AST)是一种树状的数据结构,用于表示源代码的语法结构。AST 将源代码分解为具有层次结构的节点,每个节点代表代码中的一种语法元素,如表达式、语句、变量等。
AST 在编译器和分析工具中有着广泛的应用:
代码解析 :编译器将源代码转换成 AST,为后续的代码分析和优化提供基础。
代码分析与检查 :开发静态分析工具、代码检查工具,可以通过遍历 AST 来检查代码中的错误或改进点。
代码转化和优化 :通过修改 AST,可以实现代码的自动优化和转换,如代码格式化工具。
代码生成 :编写代码生成工具,生成新的源代码文件或中间代码。
1.3.2 如何在 Go 语言中生成和使用 AST 在 Go 语言中,我们主要用到如下三个标准库来生成和操作 AST:
**go/parser
**:用于解析 Go 源代码并生成 AST。
**go/token
**:用于管理源代码的位置和标记。
**go/ast
**:用于表示和操作 AST。
我们可以使用 parser.ParseFile
函数来解析文件并生成 AST。以下是一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "fmt" "go/ast" "go/parser" "go/token" ) func main () { src := `package main import "fmt" func main() { fmt.Println("Hello, World!") }` fset := token.NewFileSet() node, err := parser.ParseFile(fset, "" , src, parser.ParseComments) if err != nil { fmt.Println("Error:" , err) return } ast.Print(fset, node) }
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 0 *ast.File { 1 . Package: 1:1 2 . Name: *ast.Ident { 3 . . NamePos: 1:9 4 . . Name: "main" 5 . } 6 . Decls: []ast.Decl (len = 2) { 7 . . 0: *ast.GenDecl { 8 . . . TokPos: 3:1 9 . . . Tok: import 10 . . . Lparen: - 11 . . . Specs: []ast.Spec (len = 1) { 12 . . . . 0: *ast.ImportSpec { 13 . . . . . Path: *ast.BasicLit { 14 . . . . . . ValuePos: 3:8 15 . . . . . . Kind: STRING 16 . . . . . . Value: "\"fmt\"" 17 . . . . . } 18 . . . . . EndPos: - 19 . . . . } 20 . . . } 21 . . . Rparen: - 22 . . } 23 . . 1: *ast.FuncDecl { 24 . . . Name: *ast.Ident { 25 . . . . NamePos: 5:6 26 . . . . Name: "main" 27 . . . . Obj: *ast.Object { 28 . . . . . Kind: func 29 . . . . . Name: "main" 30 . . . . . Decl: *(obj @ 23) 31 . . . . } 32 . . . } 33 . . . Type: *ast.FuncType { 34 . . . . Func: 5:1 35 . . . . Params: *ast.FieldList { 36 . . . . . Opening: 5:10 37 . . . . . Closing: 5:11 38 . . . . } 39 . . . } 40 . . . Body: *ast.BlockStmt { 41 . . . . Lbrace: 5:13 42 . . . . List: []ast.Stmt (len = 1) { 43 . . . . . 0: *ast.ExprStmt { 44 . . . . . . X: *ast.CallExpr { 45 . . . . . . . Fun: *ast.SelectorExpr { 46 . . . . . . . . X: *ast.Ident { 47 . . . . . . . . . NamePos: 6:5 48 . . . . . . . . . Name: "fmt" 49 . . . . . . . . } 50 . . . . . . . . Sel: *ast.Ident { 51 . . . . . . . . . NamePos: 6:9 52 . . . . . . . . . Name: "Println" 53 . . . . . . . . } 54 . . . . . . . } 55 . . . . . . . Lparen: 6:16 56 . . . . . . . Args: []ast.Expr (len = 1) { 57 . . . . . . . . 0: *ast.BasicLit { 58 . . . . . . . . . ValuePos: 6:17 59 . . . . . . . . . Kind: STRING 60 . . . . . . . . . Value: "\"Hello, World!\"" 61 . . . . . . . . } 62 . . . . . . . } 63 . . . . . . . Ellipsis: - 64 . . . . . . . Rparen: 6:32 65 . . . . . . } 66 . . . . . } 67 . . . . } 68 . . . . Rbrace: 7:1 69 . . . } 70 . . } 71 . } 72 . FileStart: 1:1 73 . FileEnd: 7:2 74 . Scope: *ast.Scope { 75 . . Objects: map[string]*ast.Object (len = 1) { 76 . . . "main": *(obj @ 27) 77 . . } 78 . } 79 . Imports: []*ast.ImportSpec (len = 1) { 80 . . 0: *(obj @ 12) 81 . } 82 . Unresolved: []*ast.Ident (len = 1) { 83 . . 0: *(obj @ 46) 84 . } 85 . GoVersion: "" 86 }
常见的节点类型 :
**ast.File
**:代表一个源文件。
**ast.GenDecl
**:代表常规声明(如导入、常量、变量、类型声明)。
**ast.FuncDecl
**:代表函数声明。
**ast.BlockStmt
**:代表代码块。
**ast.Expr
**:所有表达式的基础节点类型,如字面量、二元表达式等。
**ast.Stmt
**:所有语句的基础节点类型,如赋值语句、返回语句等。
例如这里输出的*ast.FuncDecl
代表函数的定义,名称是 main
。内容有点多,可以参考 ast.go;l=972 自行探索。
1.3.3 遍历访问 AST 节点 生成 AST 后,可以遍历和访问各个节点。例如,以下代码展示了如何遍历 AST 并提取想要的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "go/ast" "go/parser" "go/token" "fmt" ) func main () { src := `package main func add(a int, b int) int { return a + b }` fset := token.NewFileSet() fileNode, err := parser.ParseFile(fset, "" , src, parser.ParseComments) if err != nil { fmt.Println("Error:" , err) return } ast.Inspect(fileNode, func (n ast.Node) bool { switch n := n.(type ) { case *ast.File: fmt.Printf("Package: %s\n" , n.Name.Name) case *ast.FuncDecl: fmt.Printf("Function: %s\n" , n.Name.Name) case *ast.GenDecl: fmt.Printf("General Declaration: %s\n" , n.Tok) } return true }) }
输出:
1 2 Package: main Function: add
1.4 实际应用 1.4.1 代码检查 代码检查是静态分析的重要部分,可以帮助发现潜在的问题或优化代码。通过遍历 AST,我们可以编写自定义的规则来检查代码。
以下示例展示了如何使用 AST 检查未使用的变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package mainimport ( "fmt" "go/ast" "go/parser" "go/token" ) func main () { src := `package main func main() { var unused int fmt.Println("Hello, World!") }` fset := token.NewFileSet() node, err := parser.ParseFile(fset, "" , src, parser.ParseComments) if err != nil { fmt.Println("Error:" , err) return } ast.Inspect(node, func (n ast.Node) bool { if decl, ok := n.(*ast.GenDecl); ok { for _, spec := range decl.Specs { if valueSpec, ok := spec.(*ast.ValueSpec); ok { for _, name := range valueSpec.Names { if !name.IsExported() && name.Name != "_" { fmt.Printf("Unused variable: %s\n" , name.Name) } } } } } return true }) }
1.4.2 代码格式化 代码格式化是保证代码风格一致性的重要部分。通过 AST,我们可以重新组织代码,以符合预定的格式规则。
以下示例展示了如何使用 AST 为每个函数添加注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "fmt" "go/ast" "go/parser" "go/printer" "go/token" "os" ) func main () { src := `package main func add(a int, b int) int { return a + b }` fset := token.NewFileSet() node, err := parser.ParseFile(fset, "" , src, parser.ParseComments) if err != nil { fmt.Println("Error:" , err) return } ast.Inspect(node, func (n ast.Node) bool { if decl, ok := n.(*ast.FuncDecl); ok { decl.Doc = &ast.CommentGroup{ List: []*ast.Comment{ { Slash: decl.Pos() - 1 , Text: "// Function " + decl.Name.Name, }, }, } } return true }) printer.Fprint(os.Stdout, fset, node) }
1.4.3 注释处理 注释处理可以用于提取文档、生成代码等任务。通过遍历 AST,我们可以收集和处理代码中的注释。
以下示例展示了如何提取并打印所有注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "fmt" "go/ast" "go/parser" "go/token" ) func main () { src := `package main // This is a package comment func add(a int, b int) int { // This is a function comment return a + b }` fset := token.NewFileSet() node, err := parser.ParseFile(fset, "" , src, parser.ParseComments) if err != nil { fmt.Println("Error:" , err) return } for _, comment := range node.Comments { fmt.Println(comment.Text()) } }
1.4.4 Go中的具体例子 看了上述几个例子,你一定能想到 go 的一些 tool。
在 Go 中,很多工具是基于 go/ast
(抽象语法树)库实现的,用于解析和分析 Go 源代码。以下是几个常用的基于 go/ast
实现的工具:
gofmt
gofmt
是 Go 自带的代码格式化工具,用于将 Go 代码格式化成标准的风格。它利用 go/ast
分析代码结构,将其转换为统一的格式。
例如,gofmt
自动进行缩进、对齐、空格等操作,确保 Go 代码的一致性和可读性。
go vet
go vet
是一个静态代码分析工具,能检查代码中的潜在错误或不符合规范的用法。通过 go/ast
分析代码,go vet
能检测到代码中的一些常见错误,如不匹配的 Printf
参数、不符合规则的锁操作等。
golint
golint
是 Go 语言的代码规范检查工具,帮助开发者遵循 Go 的编码规范。golint
使用 go/ast
来解析代码,查找不符合规范的编码模式,例如未命名的导出结构或未遵循惯例的命名方式。
go doc
go doc
是 Go 的文档工具,利用 go/ast
分析代码中的文档注释,生成函数、方法、类型等的文档信息。
这使得 go doc
能提供方便的文档查看方式,尤其适合查看本地代码库的文档。
gorename
gorename
是一个重命名工具,允许开发者安全地重命名 Go 代码中的标识符。它使用 go/ast
分析代码的结构和依赖关系,确保所有引用都能安全重命名。
stringer
stringer
是 Go 官方提供的代码生成工具,专门用于生成枚举类型的字符串表示。它通过 go/ast
解析代码中的常量定义,并生成相关的字符串方法,方便代码中的调试输出和日志记录。
这些工具基于 go/ast
提供了对 Go 代码的结构化分析能力,从而实现了格式化、静态检查、文档生成和代码生成等功能。
1.5 其他应用AST的工具 介绍两个项目 pingcap failpoint 和 uber-go 的 gopatch 。
failpoint 的使用是在代码里实现 Marker
函数,这些函数在正常编译时会被编译器优化去掉,所以正常运行时无额外开销:
1 2 3 4 var outerVar = "declare in outer scope" failpoint.Inject("failpoint-name-for-demo" , func (val failpoint.Value) { fmt.Println("unit-test" , val, outerVar) })
故障注入时通过 failctl 将 Marker
函数转换为故障注入函数,这里就用到了 go ast
做转换。
uber-go 的 gopatch
也非常强大,假如你的代码有很多 go func
开启的 goroutine, 你想批量加入 recover
逻辑,如果数据特别多人工加很麻烦,这时可以用 gopatcher:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var patchTemplateString = `@@ @@ + import "runtime/debug" + import "{{ Logger }}" + import "{{ Statsd }}" go func(...) { + defer func(){ + if err := recover(); err != nil { + statsd.Count1("{{ StatsdTag }}", "{{ FileName }}") + logging.Error("{{ LoggerTag }}", "{{ FileName }} recover from panic, err=%+v, stack=%v", err, string(debug.Stack())) + } + }() ... }() `
编写模板例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport ( "os" "text/template" ) type PatchTemplate struct { Statsd string Logger string StatsdTag string FileName string LoggerTag string } func main () { patchTemplate := `@@ @@ + import "runtime/debug" + import "{{ .Logger }}" + import "{{ .Statsd }}" go func(...) { + defer func(){ + if err := recover(); err != nil { + statsd.Count1("{{ .StatsdTag }}", "{{ .FileName }}") + logging.Error("{{ .LoggerTag }}", "{{ .FileName }} recover from panic, err=%+v, stack=%v", err, string(debug.Stack())) + } + }() ... }()` data := PatchTemplate{ Statsd: "github.com/yourusername/statsd" , Logger: "github.com/yourusername/logger" , StatsdTag: "error_count" , FileName: "example.go" , LoggerTag: "example_logger" , } t := template.Must(template.New("patch" ).Parse(patchTemplate)) t.Execute(os.Stdout, data) }
上面的例子可以实现自动在 go func(…) {
开头注入 recover
语句块,非常方便。
当然 gopatch
还可以做很多事情,可以尽情探索。
1.6 总结 上述内容介绍了基本的go ast
使用用法和比较成熟的工具,但是在实际工作中,遇到的都是一些介于这两者之间的自动化场景,例如自定义错误码,标准的HTTP错误码数量太少,对于一个大型服务来说这些错误码显然是不够的,所以你完全可以写一个递增的错误码自动化生成工具,来生成自定义错误码。 当然自动化工具只有在应对大量冗余工作时非常有用,它可以帮助我们节省人力并且保持专注。节省下的精力可以花费在值得投入的地方。 总之,go ast
库给予开发者很大的空间去处理 go 代码,掌握 AST 的使用,可以更高效地进行代码分析和工具开发。
更多该系列文章,参考medium链接:
https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a
English post: https://programmerscareer.com/golang-ast/ 作者:微信公众号 ,Medium ,LinkedIn ,Twitter 发表日期:原文在 2024-10-26 20:28 时创作于 https://programmerscareer.com/zh-cn/golang-ast/ 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证 )
评论