Contents

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工具是怎么使用的吧

  1. 安装go的yacc工具
    go install golang.org/x/tools/cmd/goyacc@latest

假如你有个叫作gopher.y的文件定义了一个新语言的语法。现在我需要使用yacc读取这个语法文件生成新语言的语法解析代码,我们可以执行如下命令:

goyacc -o gopher.go -p parser gopher.y

其中-o是输出文件的文件名, parser是包名

  1. 使用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可以自动给整形常量的合集生成字符串方法。

  1. 安装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不一样的多一点是它不做依赖分析,不对构建的过程做依赖步骤分析。

总结

让机器替我们工作,这样就不用写一些重复的代码了。