go generate 生成代码

今后一段时间要研究下go generate,在官网博客上看了Rob Pike写的generating code,花了一些时间翻译了下。有几个句子翻译的是否正确有待考量,欢迎指正。

生成代码

通用计算的一个特性--图灵完备--是一个计算机程序可以编写一个计算机程序。这是一个强大的想法,尽管经常出现,但还不足够完美。例如,它是编译器定义的重要组成部分。它也是go test命令的工作原理:它扫描要测试的软件包,写出一个包含为包定制的测试工具的Go程序,然后编译并运行。现代电脑快到可以在几分之一秒完成这个看似昂贵的序列。

   还有很多程序编写程序的其他例子。例如,yacc读入一个语法描述,并写出一个程序来解析该语法。Protocol buffer“编译器”读取接口描述输出结构定义,方法和其他支持代码。各种配置工具也是这样工作的,检查元数据或环境,输出自定义的本地配置。

   因此,编写程序的程序是软件工程的重要组成部分,但是像yacc这些可以生成源代码的程序需要基础到构建过程中,以便可以编译它们的输出。当使用像Make这样的外部构建工具时,这通常可以很容易做到。但是在Go中,Go的工具从Go源中获取所有必要的构建信息,这有一个问题。没有机制可以单独地从go tool中运行yacc。

   直到现在,就是这样。

   最新的Go发布版,1.4,包含一个新命令,可以更轻松地运行这些工具。它叫做go generate,它可以通过扫描Go源码中的特殊注释来识别要运行的常规命令。了解go generate不是go build的一部分很重要。它不包含依赖关系分析,必须在运行go build之前显式运行。它旨在由Go package的作者使用,而不是其客户端。

Go generate命令很容易使用。作为一个预热,下面展示如何使用它来生成yacc语法。假设你有一个名为gopher.y的YACC输入文件,它定义了一种新语言的语法。要生成实现语法的Go源码文件,通常会调用Yacc的标准Go版本:

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

-o选项命令输出文件,-p选项指定包名。

要使go generate驱动这个过程,在同一目录中的任何一个普通(非生成).go文件中,将该注释添加的文件中的任何位置:

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

这个文本就是上面的命令,前面加上一个由go generate识别的特殊注释。注释必须从行的开始处开始,并在在//和go:generate之间没有空格。在该标记之后,该行的其余部分指定go generate运行的命令。

现在运行它。切换到源目录,运行go generate,然后go build等等。

$ cd $GOPATH/myrepo/gopher
$ go generate
$ go build
$ go test

 

   假设没有错误,go generate命令将调用yacc来创建gopher.go文件,此时目录包含完整的go源文件,因此我们可以正常构建,测试和正常工作。每次gopher.y被修改,只需要重新运行go generate来重新生成解析器。

   有关go generate如何工作的更多详细信息,包括选项,环境变量等,可以参阅设计文档。

Go generate不会影响到make或其他一些编译机制,但它依附go tool,不需要额外安装,而且很适合Go生态系统。请记住,它是为package作者,而不是客户端,只是因为它调用的程序在目标机器上可能不可用。另外,如果包含的包是通过go get导入的,一旦文件被生成(并且被测试),他就必须被检入到源码库以供客户端使用。

现在有了go generate,可以用它来做新的事情。作为一个不同寻常的如何使用go generate的例子,有一个新的程序golang.org/x/tools仓库称为stringer。它可以自动为整数常量集合编写字符串方法。它不是发行版的一部分,但它很容易安装。

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

 

Stringer文档中有个示例,假设我们有一些包含一组定义不同类型的整形常数:

 

 

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)

}

 

当然还有其他的方法来写这个功能。我们可以使用一些Pill索引的字符串,或者map,或者其他一些技术。无论我们做什么,如果我们改变Pills集合,我们需要维护它来保证它是正确的。(Paracetamol的两个Name比其他的要棘手)。另外,采取哪种方法的问题取决于类型和值:有符号还是无符号,密集还是稀疏,基于零还是不基于零的等等。

 

Stringer程序负责处理所有这些细节。虽然它可以独立运行,但是它是由go generate驱动的要使用它,可以向源代码中添加生成注释,类型定义附近。

//go:generate stringer -type=Pill

 

此规则制定go generate 应运行stringer工具以生成Pill类型的String方法。输出会自动写入pill_string.go(默认情况下,我们可以使用 -output标志来覆盖)

运行之后

$ go generate

$ cat pill_string.go

// generated by stringer -type Pill pill.go; DO NOT EDIT

 

package pill

 

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]]

}

$

每次更改Pill或常量的定义时,我们需要做的就是运行go generate来更新String方法。当然,如果我们在同一个包中有多种类型设置了这种方式,单个命可以更新所有的String方法。

毫无疑问,生成的方法是丑的。但是,那是可以接受的,因为人类不需要做与它相关的工作,机器生成的代码通常是丑的。它努力做到高效率。所有的名称都在一个单一的字符串中,这样可以节省内存(即使有数十个也只有一个字符串头)。然后,一个数组,_Pill_index,通过一个简单高效的技术从值映射到名称。也请注意,_Pill_index是uint8的一个数组(不是一个切片),这是一个足够跨越值空间的最小整数。如果有更多的值,或者更少的,那么生产的_Pill_index类型可能会改变为uint16或者int8;无论什么都效果很好。

Stringer生成的Method使用的方法会根据常量集合的属性而变化。例如,如果常量是稀疏的,它可能会使用一个map。下面是一个基于常数集的常见例子,它代表了另外一种,

 

const _Power_name = "p0p1p2p3p4p5..."

 

var _Power_map = map[Power]string{

    1:    _Power_name[0:2],

    2:    _Power_name[2:4],

    4:    _Power_name[4:6],

    8:    _Power_name[6:8],

    16:   _Power_name[8:10],

    32:   _Power_name[10:12],

    ...,

}

 

func (i Power) String() string {

    if str, ok := _Power_map[i]; ok {

        return str

    }

    return fmt.Sprintf("Power(%d)", i)

}

简而言之,自动生成的method可以做到比人类做地更好。

在go tree中已经安装了go generate的许多其他用途。包括在unicode包中生产Unicode表,为encoding/gob创建有效的编解码方法,在time包中创建时区数据等等。

请创造性的使用go generate,鼓励动手实践。

 

即使没有,使用新的stringer工具为您的整形常量编写String方法。让机器做这样的工作。

 

By Rob Pike

posted @ 2017-03-31 22:43  majianguo  阅读(11719)  评论(0编辑  收藏  举报