• 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

  • 相关阅读:
    Django路由系统
    修改数据库时区问题
    Django框架篇
    前端css
    前端html
    前端初识
    数据库3
    数据库2
    数据库1
    数据库初识
  • 原文地址:https://www.cnblogs.com/majianguo/p/6653919.html
Copyright © 2020-2023  润新知