go generate
生成代码
用代码来生成代码是通用计算的一个重要属性。这个想法可能不是那么受人重视,但实际上确经常在发生。其实这基本上就是编译器的定义。go test
也使用了这种设计思路:运行test的时候他会扫描一遍代码中需要被测试的内容,然后生成一段go代码来包裹这些测试代码使他能够执行起来,然后编译生成的代码并执行。只不过现代计算机运算速度非常快,用户对于这个过程没有明显的感知。
还有很多代码生成代码的例子。比如Yacc就是读取 语法描述,然后生成一个可以解析这种语法描述的代码。广义上来说所有可配置的工具都是这么执行的,读取配置,然后依据配置中的这些上下文环境,执行一个特定的逻辑。
用代码生成代码在软件工程中是一个重要的组成部分。类似yacc的生成的代码会被加入到构建流程中的会相对复杂很对。如果依赖一些外部的构建工具可能会简单些。在go 1.4之前,如果只是使用go tool 你没法获取到足够的关于go 源码的信息。
go generate
go 1.4引入了go generate的机制,可以通过扫描源代码中特殊的注解来识别要执行的命令。go generate不是go build的一部分,必须在go build之前手动执行。一开始设计的时候是用来给go package的作者使用的,而不是用户。
yacc
先看下Go的yacc工具是怎么使用的吧
- 安装go的yacc工具
go install golang.org/x/tools/cmd/goyacc@latest
假如你有个叫作gopher.y
的文件定义了一个新语言的语法。现在我需要使用yacc
读取这个语法文件生成新语言的语法解析代码,我们可以执行如下命令:
goyacc -o gopher.go -p parser gopher.y
其中-o是输出文件的文件名, parser是包名
- 使用
go generate
在任意的常规(非生成)go源代码文件中加入下列注解
//go:generate goyacc -o gopher.go -p parser gopher.y
注解必须以//开头,然后是go:generate。后面就是要执行的命令。 我们可以在我们的项目下使用go generate了
$ cd $GOPATH/myrepo/gopher
$ go generate
$ go build
$ go test
如果没有问题的话我们就可以使用go generate 生成的gopher.go文件来进行构建和测试了。每次语法定义有改变只要修改之后重新执行go generate就可以了。
go generate 就是go语言中的make工具,不需要使用make也可以在go生态中使用代码来生成代码。主要还是给包的开发者使用,而不是使用方。
stringer
看下另一个stringer的例子,stringer可以自动给整形常量的合集生成字符串方法。
- 安装stringer
go install golang.org/x/tools/cmd/stringer@latest
假如我们有下面的定义:
package painkiller
type Pill int
const (
Placebo Pill = iota //安慰剂
Aspirin //阿司匹林
Ibuprofen //布洛芬
Paracetamol //对乙酰氨基酚, 扑热息痛, 醋氨酚, 药〉扑热息痛
Acetaminophen = Paracetamol //对乙酰氨基酚
)
为了测试方便,我们需要能够清晰地打印这些变量,我们需要类似下面的方法
func (p Pill) String() string
如果手写地话我们可以这样实现
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)
}
这只是一个例子,我们当然可以用切片,map或者其他的方式来实现这个能力,但是每次pill的定义有变化的时候我们都会需要重新修改这段代码,手动维护是一件麻烦的事情。
stringer就是用来解决这些问题的。可以在源码中添加下面的注解
//go:generate stringer -type=Pill
go generate 就会执行stringer来给Pill类型来生成字符串方法。默认的输出文件是 pill_string.go
试试看:
$ go generate
$ cat pill_string.go
// Code 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]]
}
$
存储了一个字符串,以及每个常量对应的数组下标。乍一看生成的代码非常丑陋,可读性不是很好,但是生成代码的效率很高,主要讨论的是go generate,这里就不继续分析这段生成的代码了。 每次修改Pill的定义或者常量定义的时候都能通过
go generate
来更新这个字符串方法。
在Go中还有许多其他使用go generate的例子。例如,unicode包中生成Unicode表,encoding/gob中创建高效的数组编码和解码方法,time包中生成时区数据等等。
设计初衷
go generate设计的初衷是用来在go build执行之前做一些代码的预处理,一些使用场景
- yacc ,生成语法解析
- protobufs ,生成对应的go 代码
- Unicode: 从unicodeData.txt 生成 表格
- string: 例如上面提到的为不同类型的常量生成
string()
方法 - 宏:从通用的包生成特定的实现
go generate 和make不一样的多一点是它不做依赖分析,不对构建的过程做依赖步骤分析。
总结
让机器替我们工作,这样就不用写一些重复的代码了。