generate命令

go generate命令是从Go1.4开始才设计的,用于在编译前自动化生成某类代码。go generatego build是完全不一样的命令,通过分析源码中特殊的注释,然后执行相应的命令。

这些命令都是很明确的,没有任何的依赖在里面。这个go generate是给源码开发人员用的,不是给使用这个包的人用的,是方便你来生成一些代码的。

有几点需要注意:

  1. 该特殊注释必须在.go源码文件中。
  2. 每个源码文件可以包含多个generate特殊注释时,显式运行go generate命令时,才会执行特殊注释后面的命令。
  3. 命令串行执行的,如果出错,就终止后面的执行。
  4. 特殊注释必须以"//go:generate"开头,双斜线后面没有空格。

例如,使用yacc来生成代码,可能如下命令:

go tool yacc -o gopher.go -p parser gopher.y

-o 指定了输出的文件名,-p指定了package的名称,这是一个单独的命令,如果我们想用go generate来触发这个命令,那么就可以在当前目录的任意一个xxx.go文件里面的任意位置增加一行如下的注释:

//go:generate go tool yacc -o gopher.go -p parser gopher.y

//go:generate 是没有任何空格的,这其实就是一个固定的格式,在扫描源码文件的时候就是根据这个来判断的。

这时可以通过如下命令来生成、编译、测试。如果gopher.y文件有修改,那么就重新执行go generate重新生成文件就好。

go generate
go build
go test

在下面这些场景下,我们会使用go generate命令(不完整列表):

  • goyacc Go的Yacc。从.y文件生成.go文件
  • stringer 实现fmt.Stringer枚举的接口。
  • gostringer fmt.GoStringer为枚举实现接口。
  • jsonenums 枚举的实现json.Marshalerjson.Unmarshaler接口。
  • go-syncmap 使用软件包作为的通用模板生成Go代码sync.Map
  • go-syncpool 使用软件包作为的通用模板生成Go代码sync.Pool
  • go-atomicvalue 使用软件包作为的通用模板生成Go代码atomic.Value
  • go-nulljson 使用包作为实现database/sql.Scanner和的通用模板生成Go代码database/sql/driver.Valuer
  • go-enum 使用包作为实现接口的通用模板生成Go代码fmt.Stringer|binary|json|text|sql|yaml枚举。
  • go-import 执行非go文件的自动导入。
  • gojson 从示例json文档生成go结构定义。
  • vfsgen 生成静态实现给定虚拟文件系统的vfsdata.go文件。
  • goreuse 使用包作为通用模板通过替换定义来生成Go代码。
  • embedfiles 将文件嵌入Go代码。
  • ragel 状态机编译器
  • peachpy 嵌入在Python中的x86-64汇编器,生成Go汇编
  • bundle Bundle创建适用于包含在特定目标软件包中的源软件包的单一源文件版本。
  • msgp MessagePack的Go代码生成器
  • protobufprotocol buffer定义文件.proto生成 .pb.go文件
  • thriftrw thrift
  • gogen-avro avro
  • swagger-gen-types 从swagger定义中去生成代码
  • avo 使用Go生成汇编代码
  • Wire Go的编译时依赖注入
  • sumgen 从sum-type声明生成接口方法实现
  • interface-extractor 生成所需类型的接口,仅在包内使用方法。
  • deep-copy 为给定类型创建深度复制方法。
  • UnicodeUnicodeData.txt生成Unicode
  • HTMLHTML文件嵌入到Go源码
  • binddata 将形如JPEG这样的文件转成Go代码中的字节数组。
  • 宏 为既定的包生成特定的实现,比如用于intssort.Ints

在hubble中存在大量的probuf,yacc,stringer自动生成,同时makefile文件中也有相关的构建脚本。

go generate命令格式如下所示:

go generate [-run regexp] [-n] [-v] [-x] [command] [build flags] [file.go... | packages]

参数说明如下:

  • -run 正则表达式匹配命令行,仅执行匹配的命令;
  • -v 输出被处理的包名和源文件名;
  • -n 显示不执行命令;
  • -x 显示并执行命令;
  • command 可以是在环境变量 PATH 中的任何命令。

执行go generate命令时,也可以使用一些环境变量,如下所示:

  • $GOARCH体系架构(arm、amd64 等);
  • $GOOS当前的 OS 环境(linux、windows 等);
  • $GOFILE当前处理中的文件名;
  • $GOLINE 当前命令在文件中的行号;
  • $GOPACKAGE 当前处理文件的包名;

用法

自动化

实现类似于shell脚本的功能。

示例

假设我们有一个main.go文件,内容如下:

package main

import "fmt"

//go:generate go run main.go
//go:generate go version
func main() {
  fmt.Println("Hello World!")
}

执行go generate -x命令,输出结果如下:

go run main.go
Hello World!
go version
go version go1.13.4 linux/amd64

通过运行结果可以看出,相当于写了一个顺序执行go run main.gogo version的脚本。这里-x参数是为了将执行的具体命令打印出来。

枚举

参照官方博客,例子在代码

在定义枚举时,如果增加一个新的枚举类型,可能需要修改大量的方法,而使用了go:generate stringer可以简化这个过程,再增加新枚举类型时可能只需要修改枚举定义即可。

这里介绍另一个例子,例如有一个错误定义:

const (
    ERR_CODE_OK = 0 // OK
    ERR_CODE_INVALID_PARAMS = 1 // invalid parameter
    ERR_CODE_TIMEOUT = 2 // time out
    // ...
)

如果需要将错误码和描述信息对应上,可能需要做:

var mapErrDesc = map[int]string {
    ERR_CODE_OK: "OK",
    ERR_CODE_INVALID_PARAMS: "invalid parameter",
    ERR_CODE_TIMEOUT: "time out",
    // ...
}

func GetDescription(errCode int) string {
    if desc, exist := mapErrDesc[errCode]; exist {
        return desc
    }
    
    return fmt.Sprintf("error code: %d", errCode)
}

这时每次添加新的错误码时除了要修改错误码时也需要修改map。

使用go generate方式,如果本地没有,首先安装stringer包。

git clone https://github.com/golang/tools/ $GOPATH/src/golang.org/x/tools
go install golang.org/x/tools/cmd/stringer

代码修改为:

type ErrCode int

//go:generate stringer -type ErrCode -linecomment
const (
	ERR_CODE_OK             ErrCode = 0 // OK
	ERR_CODE_INVALID_PARAMS ErrCode = 1 // invalid parameter
	ERR_CODE_TIMEOUT        ErrCode = 2 // time out
	// ...
)

执行go generate命令,这时会自动生成文件,包含一个string()方法,这时使用它就可以得到对应的错误描述了。再修改枚举时只需要修改枚举类型本身即可。

复杂例子可以参考hubble或者jsonenums。jsonenums是一个用于为枚举类型自动生成JSON编组样板代码的类库。在Go标准类库里面已经有大量可以用于解析AST的接口,而AST使得编写元编程工具更简单,更容易。