go generate 预处理器教程

Jan 23 2019 Golang

通常编译 protobuf 会使用 protoc 手动编译,更好一点可以写一个 Makefile 指令来做。

不过在 Go 中提供一种在源文件定义类似 Makefile 指令 generate。当运行 go generate 后,编译器会找到所有包含 //go:generate command argument... 的注释,然后运行后面的命令。

那这样的话就不需要再写一个 Makefile 指令了。

使用 go generate 工具编译 protobuf

新建一个文件目录 test,然后编辑 doc.go 文件。BTW,doc.go 是约定俗成写包文档的文件,通常不会写逻辑代码,所以这里写 generate 指令最好不过了。

1
2
3
//go:generate protoc --go_out=. *.proto

package test

generate 指令只能在 go 文件中使用,而且需要注意的是和传统注释不同的是 // 后面不能有空格。

然后编辑 test.proto 文件

1
2
3
4
5
6
7
syntax="proto3";

message Info {
uint32 info_id = 1;
string title = 2;
string content = 3;
}

另外 go build 等其它命令不会调用 go generate,必须手动显式调用 go generate 。不过这报错了,提示找不到文件。

1
2
*.proto: No such file or directory
doc.go:1: running "protoc": exit status 1

这个问题在文档里有说明,generate 并不处理 glob。那我们这里修改 doc.go 当 sh 直接处理就行了。

1
2
3
//go:generate sh -c "protoc --go_out=. *.proto"

package test

另外也要注意,双引号会被 go 进行解析,所以该转义的地方需要注意转义。

自动生成 Stringer 接口

在 golang 博客中 generate code介绍了一种类似宏指令的方式。

假设定义一组不同类型的药物的枚举:

1
2
3
4
5
6
7
8
9
10
11
package painkiller

type Pill int

const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)

通常为了能直接 print 其枚举名称,我们会给 Pill 实现 Stringer 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
func (p Pill) String() string {
switch p {
case Placebo:
return "Placebo"
case Aspirin:
return "Aspirin"
case Ibuprofen:
return "Ibuprofen"
case Paracetamol: // == Acetaminophen
return "Paracetamol"
}
return fmt.Sprintf("Pill(%d)", p)
}

不过有了 generate 指令我们可以不用手写这些逻辑代码。

下载并安装 stringer

1
go get golang.org/x/tools/cmd/stringer

在 Pill 包名称上添加一句 //go:generate stringer -type=Pill。通常为了和文档区分开,我们还要加一个空行。

1
2
3
4
5
6
7
8
9
10
11
12
13
//go:generate stringer -type=Pill

package painkiller

type Pill int

const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)

这时候会自动生成 pill_string.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// generated by stringer -type Pill pill.go; DO NOT EDIT

package painkiller

import "fmt"

const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"

var _Pill_index = [...]uint8{0, 7, 14, 23, 34}

func (i Pill) String() string {
if i < 0 || i+1 >= Pill(len(_Pill_index)) {
return fmt.Sprintf("Pill(%d)", i)
}
return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}

命令格式

1
2
3
4
5
6
7
go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]

-v 输出被处理的包名和源文件名
-n 显示不执行命令
-x 显示并执行命令

-run 正则匹配要运行的指令

还可以在命令中定义别名,不过只有当前文件内有效。

1
2
//go:generate -command YACC go tool yacc
//go:generate YACC -o test.go -p parse test.y

另外还支持下面这些变量:

1
2
3
4
5
6
7
8
9
10
11
12
$GOARCH
CPU架构 (arm、amd64等)
$GOOS
操作系统类型(linux、windows等)
$GOFILE
当前处理中的文件名(就是文件名,不包含路径)
$GOLINE
当前命令在文件中的行号
$GOPACKAGE
当前处理文件的包名(只是当前包名,不包含路径)
$DOLLAR
美元符号