0%

《Golang》Generate 生成器

元编程

元编程(Metaprogramming)是指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。

很多情况下与手工编写全部代码相比工作效率更高。编写元程序的语言称之为元语言,被操作的语言称之为目标语言。

编译器就是一个典型的元编程工具,Golang编译器是Golang写的,所以元语言就是Golang,被操作的语言同样是Golang,所以目标语言也是Golang

Golang是自举的,可以自己编译自己,编译器就是Golang写的,所以编译相关的包很完善

这就使得人们可以很方便的使用官方提供的一些包来解析Go语言得到抽象语法树,然后通过抽象语法树生成任何想要的一些代码

这就是Golang的Generate生成器,你可以通过编写自己的生成器来做一些繁琐的工作,解放你的双手

官方编写了一些常用的生成器,比如stringer

stringer生成器介绍

stringer,用来为类型生成String方法

举个例子,你的web服务端声明了一堆错误码,很多时候你需要将错误码返回给前端,你可能是这么做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"


const (
ERRORCODE_TEST1 int64 = 1000
ERRORCODE_TEST2 int64 = 1001
)

var ErrDescs = map[int64]string{
ERRORCODE_TEST1: "TEST1错误码",
ERRORCODE_TEST2: "TEST2错误码",
}

func response () string {
// ...
return ErrDescs[ERRORCODE_TEST1]
}

func main() {
fmt.Println(response())
}

上面的代码中,当你再加一个错误码,你就必须在Map相应添加一个键值

你可能觉得这工作量已经很小了,应该不能精简了吧,其实不然,使用stringer生成器,可以改成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//go:generate stringer -type ErrorCode -linecomment

package main

import "fmt"

type ErrorCode int64

const (
ERRORCODE_TEST1 ErrorCode = 1000 // TEST1错误码
ERRORCODE_TEST2 ErrorCode = 1001 // TEST2错误码
)

func response () string {
// ...
return fmt.Sprintf("%s", ERRORCODE_TEST1)
}

func main() {
fmt.Println(response())
}

运行 go generate ./cmd/main 即可生成以下代码

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
// Code generated by "stringer -type ErrorCode -linecomment"; DO NOT EDIT.

package main

import "strconv"

func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ERRORCODE_TEST1-1000]
_ = x[ERRORCODE_TEST2-1001]
}

const _ErrorCode_name = "TEST1错误码TEST2错误码"

var _ErrorCode_index = [...]uint8{0, 14, 28}

func (i ErrorCode) String() string {
i -= 1000
if i < 0 || i >= ErrorCode(len(_ErrorCode_index)-1) {
return "ErrorCode(" + strconv.FormatInt(int64(i+1000), 10) + ")"
}
return _ErrorCode_name[_ErrorCode_index[i]:_ErrorCode_index[i+1]]
}

从此你添加错误码后只需要在后面多加一个注释即可

go generate

generate 是 go 提供的子命令,当你执行它时

它会遍历指定包下所有文件,找到是否有 //go:generate 前缀的行,有的话执行它

注意上面代码的第一行 //go:generate stringer -type ErrorCode -linecomment

这一行是 go generate 识别并执行的,go:generate 就是 go generate ,它前面不能有空格,不然就是普通的注释了

紧跟在 go:generate 后面的就是要执行的命令,go generate 的参数会尽数加到这个命令后面

go generate ./cmd/main 就相当于执行了 stringer -type ErrorCode -linecomment ./cmd/main

stringer 则是一个生成器工具,需要下载安装的,安装方法:go get golang.org/x/tools/cmd/stringer

下面通过源码来看一下 stringer生成器 的原理

stringer生成器源码浅析

首先看入口

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
func main() {
log.SetFlags(0)
log.SetPrefix("stringer: ")
flag.Usage = Usage
flag.Parse()
if len(*typeNames) == 0 {
flag.Usage()
os.Exit(2)
}
types := strings.Split(*typeNames, ",")
var tags []string
if len(*buildTags) > 0 {
tags = strings.Split(*buildTags, ",")
}

// We accept either one directory or a list of files. Which do we have?
args := flag.Args()
if len(args) == 0 { // 如果没有传参数(包名),那么就是当前目录所在的包
// Default: process whole package in current directory.
args = []string{"."}
}

// Parse the package once.
var dir string
g := Generator{
trimPrefix: *trimprefix,
lineComment: *linecomment,
}
// TODO(suzmue): accept other patterns for packages (directories, list of files, import paths, etc).
if len(args) == 1 && isDirectory(args[0]) {
dir = args[0]
} else {
if len(tags) != 0 {
log.Fatal("-tags option applies only to directories, not when files are specified")
}
dir = filepath.Dir(args[0])
}

g.parsePackage(args, tags) // parse包

// 开始生成代码
// Print the header and package clause.
g.Printf("// Code generated by \"stringer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " ")) // 写入缓存
g.Printf("\n")
g.Printf("package %s", g.pkg.name)
g.Printf("\n")
g.Printf("import \"strconv\"\n") // Used by all methods.

// Run generate for each type.
for _, typeName := range types { // 生成每个指定类型对应的代码
g.generate(typeName)
}

// Format the output.
src := g.format() // 格式化生成的代码

// Write to file.
outputName := *output
if outputName == "" {
baseName := fmt.Sprintf("%s_string.go", types[0])
outputName = filepath.Join(dir, strings.ToLower(baseName))
}
err := ioutil.WriteFile(outputName, src, 0644) // 写入文件
if err != nil {
log.Fatalf("writing output: %s", err)
}
}

接下来看 g.generate(typeName) 是怎么为每个类型生成代码的

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
func (g *Generator) generate(typeName string) {  // 针对某个指定类型遍历包中所有文件并生成代码
values := make([]Value, 0, 100)
for _, file := range g.pkg.files { // 遍历包中的每一个文件(ast.File类型)
// Set the state for this run of the walker.
file.typeName = typeName // 设置上需要操作的类型名,file.genDecl函数中会用到
file.values = nil
if file.file != nil {
ast.Inspect(file.file, file.genDecl) // 内窥这个文件(就是遍历这个文件的抽象语法树),得到文件中所有目标类型的变量
values = append(values, file.values...)
}
}

if len(values) == 0 {
log.Fatalf("no values defined for type %s", typeName)
}
// Generate code that will fail if the constants change value.
g.Printf("func _() {\n")
g.Printf("\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n")
g.Printf("\t// Re-run the stringer command to generate them again.\n")
g.Printf("\tvar x [1]struct{}\n")
for _, v := range values {
g.Printf("\t_ = x[%s - %s]\n", v.originalName, v.str)
}
g.Printf("}\n")
runs := splitIntoRuns(values) // 这里到下面开始生成目标类型的String方法
// The decision of which pattern to use depends on the number of
// runs in the numbers. If there's only one, it's easy. For more than
// one, there's a tradeoff between complexity and size of the data
// and code vs. the simplicity of a map. A map takes more space,
// but so does the code. The decision here (crossover at 10) is
// arbitrary, but considers that for large numbers of runs the cost
// of the linear scan in the switch might become important, and
// rather than use yet another algorithm such as binary search,
// we punt and use a map. In any case, the likelihood of a map
// being necessary for any realistic example other than bitmasks
// is very low. And bitmasks probably deserve their own analysis,
// to be done some other day.
switch {
case len(runs) == 1:
g.buildOneRun(runs, typeName)
case len(runs) <= 10:
g.buildMultipleRuns(runs, typeName)
default:
g.buildMap(runs, typeName)
}
}

生成器需要从文件中找到所有使用了目标类型的所有变量,并且掌握他们的初始值,这样String()函数才能得到不同变量的不同对应字符串

这些工作是在 file.genDecl 中完成的,下面看看

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
88
89
90
91
92
93
func (f *File) genDecl(node ast.Node) bool {  // 处理文件对应抽象语法树中每一个节点
decl, ok := node.(*ast.GenDecl) // 抽象语法树的节点转化成ast.GenDecl(import, constant, type or variable declaration)
if !ok || decl.Tok != token.CONST { // 如果转化失败或者不是const声明,则直接不处理
// We only care about const declarations.
return true
}
// The name of the type of the constants we are declaring.
// Can change if this is a multi-element declaration.
typ := ""
// Loop over the elements of the declaration. Each element is a ValueSpec:
// a list of names possibly followed by a type, possibly followed by values.
// If the type and value are both missing, we carry down the type (and value,
// but the "go/types" package takes care of that).
for _, spec := range decl.Specs { // const里面可以声明多个变量,这里是遍历他们
vspec := spec.(*ast.ValueSpec) // Guaranteed to succeed as this is CONST.
if vspec.Type == nil && len(vspec.Values) > 0 { // 如果没有指定类型且给了初始值(const X = 1),则记住这个值的类型
// "X = 1". With no type but a value. If the constant is untyped,
// skip this vspec and reset the remembered type.
typ = ""

// If this is a simple type conversion, remember the type.
// We don't mind if this is actually a call; a qualified call won't
// be matched (that will be SelectorExpr, not Ident), and only unusual
// situations will result in a function call that appears to be
// a type conversion.
ce, ok := vspec.Values[0].(*ast.CallExpr)
if !ok {
continue
}
id, ok := ce.Fun.(*ast.Ident)
if !ok {
continue
}
typ = id.Name
}
if vspec.Type != nil { // 如果指定了类型(const X T = 1),则直接记住这个类型
// "X T". We have a type. Remember it.
ident, ok := vspec.Type.(*ast.Ident)
if !ok {
continue
}
typ = ident.Name
}
if typ != f.typeName { // 如果上面记住的类型不是我要处理的类型,则忽略
// This is not the type we're looking for.
continue
}
// We now have a list of names (from one line of source code) all being
// declared with the desired type.
// Grab their names and actual values and store them in f.values.
for _, name := range vspec.Names { // 遍历这个const句子中的每一个变量,比如 const X,X1 T = 1 中的 X、X1
if name.Name == "_" { // 不处理 _
continue
}
// This dance lets the type checker find the values for us. It's a
// bit tricky: look up the object declared by the name, find its
// types.Const, and extract its value.
obj, ok := f.pkg.defs[name] // 找到定义。对于X,`const X T`就是他的定义
if !ok {
log.Fatalf("no value for constant %s", name)
}
info := obj.Type().Underlying().(*types.Basic).Info()
if info&types.IsInteger == 0 { // 如果不是Integer类型的,则不处理
log.Fatalf("can't handle non-integer constant type %s", typ)
}
value := obj.(*types.Const).Val() // Guaranteed to succeed as this is CONST. 这里拿出了X的值1
if value.Kind() != constant.Int { // 值的类型不是Integer则报错
log.Fatalf("can't happen: constant is not an integer %s", name)
}
i64, isInt := constant.Int64Val(value) // 判断是否int
u64, isUint := constant.Uint64Val(value) // 判断是否uint
if !isInt && !isUint { // 如果都不是,报错
log.Fatalf("internal error: value of %s is not an integer: %s", name, value.String())
}
if !isInt { // 如果是int,则转换成uint
u64 = uint64(i64)
}
v := Value{
originalName: name.Name,
value: u64,
signed: info&types.IsUnsigned == 0,
str: value.String(),
}
if c := vspec.Comment; f.lineComment && c != nil && len(c.List) == 1 {
v.name = strings.TrimSpace(c.Text())
} else {
v.name = strings.TrimPrefix(v.originalName, f.trimPrefix)
}
f.values = append(f.values, v) // 把这个变量的信息放入缓存中
}
}
return false
}

总结

  1. go generate 会遍历指定包下所有文件,找到是否有 //go:generate 前缀的行,有的话执行它
  2. 生成器就是通过编译器前端相关包得到各个文件的抽象语法树,然后分析处理后生成想要的代码



微信关注我,及时接收最新技术文章