上一章中对于golang的内存管理说明如下:

  • 1 内存分配器
  • 2 垃圾收集器
  • 3 栈内存管理

接下来我们来对golang的元编程进行说明,主要内容有:

  • 1 插件系统
  • 2 代码生成

— — — — — — — — — — — — — — — — — — — — — — — — — — — —

图灵完备的一个重要特性是计算机程序可以生成另一个程序1,很多人可能认为生成代码在软件中并不常见,但是实际上它在很多场景中都扮演了重要的角色。Go 语言中的测试就使用了代码生成机制,go test 命令会扫描包中的测试用例并生成程序、编译并执行它们,我们在这一节中就会介绍 Go 语言中的代码生成机制。

2.1 设计原理

元编程(Metaprogramming)是计算机编程中一个非常重要、也很有趣的概念,维基百科上将元编程描述成一种计算机程序可以将代码看待成数据的能力2。

Metaprogramming is a programming technique in which computer programs have the ability to treat programs as their data.

如果能够将代码看做数据,那么代码就可以像数据一样在运行时被修改、更新和替换;元编程赋予了编程语言更加强大的表达能力,能够让我们将一些计算过程从运行时挪到编译时、通过编译期间的展开生成代码或者允许程序在运行时改变自身的行为。总而言之,元编程其实是一种使用代码生成代码的方式,无论是编译期间生成代码,还是在运行时改变代码的行为都是『生成代码』的一种3。

图 - 元编程的使用

现代的编程语言大都会为我们提供不同的元编程能力,从总体来看,根据『生成代码』的时机不同,我们将元编程能力分为两种类型,其中一种是编译期间的元编程,例如:宏和模板;另一种是运行期间的元编程,也就是运行时,它赋予了编程语言在运行期间修改行为的能力,当然也有一些特性既可以在编译期实现,也可以在运行期间实现。

Go 语言作为编译型的编程语言,它提供了比较有限的运行时元编程能力,例如:反射特性,然而由于性能的问题,反射在很多场景下都不被推荐使用。当然除了反射之外,Go 语言还提供了另一种编译期间的代码生成机制 — go generate,它可以在代码编译之前根据源代码生成代码。

2.2 代码生成

Go 语言的代码生成机制会读取包含预编译指令的注释,然后执行注释中的命令读取包中的文件,它们将文件解析成抽象语法树并根据语法树生成新的 Go 语言代码和文件,生成的代码会在项目的编译期间与其他代码一起编译和运行。

//go:generate command argument...

go generate 不会被 go build 等命令自动执行,该命令需要显式的触发,手动执行该命令时会在文件中扫描上述形式的注释并执行后面的执行命令,需要注意的是 go:generate 和前面的 // 之间没有空格,这种不包含空格的注释一般是 Go 语言的编译器指令,而我们在代码中的正常注释都应该保留这个空格4。

代码生成最常见的例子就是官方提供的 stringer5,这个工具可以扫描如下所示的常量定义,然后为当前常量类型 Piller 生成对应的 String() 方法:

// pill.go
package painkiller//go:generate stringer -type=Pill
type Pill int
const (Placebo Pill = iotaAspirinIbuprofenParacetamolAcetaminophen = Paracetamol
)

当我们在上述文件中加入 //go:generate stringer -type=Pill 注释并调用 go generate 命令时,在同一目录下会出现如下所示的 pill_string.go 文件,该文件中包含两个函数,分别是 _String

// Code generated by "stringer -type=Pill"; DO NOT EDIT.package painkillerimport "strconv"func _() {// An "invalid array index" compiler error signifies that the constant values have changed.// Re-run the stringer command to generate them again.var x [1]struct{}_ = x[Placebo-0]_ = x[Aspirin-1]_ = x[Ibuprofen-2]_ = x[Paracetamol-3]
}const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"var _Pill_index = [...]uint8{0, 7, 14, 23, 34}func (i Pill) String() string {if i < 0 || i >= Pill(len(_Pill_index)-1) {return "Pill(" + strconv.FormatInt(int64(i), 10) + ")"}return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}

这段生成的代码很值得我们学习,它通过编译器的检查提供了非常健壮的 String 方法。我们在这里不展示具体的使用过程,本节将重点分析从执行 go generate 到生成对应 String 方法的整个过程,帮助各位理解代码生成机制的工作原理,代码生成的过程可以分成以下两个部分:

  1. 扫描 Go 语言源文件,查找待执行的 //go:generate 预编译指令;
  2. 执行预编译指令,再次扫描源文件并根据源文件中的代码生成代码;

预编译指令

当我们在命令行中执行 go generate 命令时,它会调用源代码中的 cmd/go/internal/generate.runGenerate 函数扫描包中的预编译指令,该函数会遍历命令行传入包中的全部文件并依次调用 cmd/go/internal/generate.generate

func runGenerate(cmd *base.Command, args []string) {...for _, pkg := range load.Packages(args) {...pkgName := pkg.Namefor _, file := range pkg.InternalGoFiles() {if !generate(pkgName, file) {break}}pkgName += "_test"for _, file := range pkg.InternalXGoFiles() {if !generate(pkgName, file) {break}}}
}

cmd/go/internal/generate.generate 函数会打开传入的文件并初始化一个用于扫描 cmd/go/internal/generate.Generator 的结构体:

func generate(pkg, absFile string) bool {fd, err := os.Open(absFile)if err != nil {log.Fatalf("generate: %s", err)}defer fd.Close()g := &Generator{r:        fd,path:     absFile,pkg:      pkg,commands: make(map[string][]string),}return g.run()
}

结构体 cmd/go/internal/generate.Generator 的私有方法 cmd/go/internal/generate.Generator.run 会在对应的文件中扫描指令并执行,该方法的实现原理很简单,我们在这里简单展示一下该方法的简化实现:

func (g *Generator) run() (ok bool) {input := bufio.NewReader(g.r)for {var buf []bytebuf, err = input.ReadSlice('n')if err != nil {if err == io.EOF && isGoGenerate(buf) {err = io.ErrUnexpectedEOF}break}if !isGoGenerate(buf) {continue}g.setEnv()words := g.split(string(buf))g.exec(words)}return true
}

上述代码片段会按行读取被扫描的文件并调用 cmd/go/internal/generate.isGoGenerate 判断当前行是否以 //go:generate 注释开头,如果该行确定以 //go:generate 开头,那么就会解析注释中的命令和参数并调用 cmd/go/internal/generate.Generator.exec 运行当前命令。

抽象语法树

stringer 充分利用了 Go 语言标准库对编译器各种能力的支持,其中包括用于解析抽象语法树的 go/ast、用于格式化代码的 go/fmt 等,Go 通过标准库中的这些包对外直接提供了编译器的相关能力,让使用者可以直接在它们上面构建复杂的代码生成机制并实施元编程技术。

作为二进制文件,stringer 命令的入口就是如下所示的 main 函数,在下面的代码中,我们初始化了一个用于解析源文件和生成代码的 Generator,然后开始拼接生成的文件:

func main() {types := strings.Split(*typeNames, ",")...g := Generator{trimPrefix:  *trimprefix,lineComment: *linecomment,}...g.Printf("// Code generated by "stringer %s"; DO NOT EDIT.n", strings.Join(os.Args[1:], " "))g.Printf("n")g.Printf("package %s", g.pkg.name)g.Printf("n")g.Printf("import "strconv"n")for _, typeName := range types {g.generate(typeName)}src := g.format()baseName := fmt.Sprintf("%s_string.go", types[0])outputName = filepath.Join(dir, strings.ToLower(baseName))if err := ioutil.WriteFile(outputName, src, 0644); err != nil {log.Fatalf("writing output: %s", err)}
}

从这段代码中我们能看到最终生成文件的轮廓,最上面的调用的几次 Generator.Printf 会在内存中写入文件头的注释、当前包名以及引入的包等,随后会为待处理的类型依次调用 Generator.generate,这里会生成一个签名为 _ 的函数,通过编译器保证枚举类型的值不会改变:

func (g *Generator) generate(typeName string) {values := make([]Value, 0, 100)for _, file := range g.pkg.files {file.typeName = typeNamefile.values = nilif file.file != nil {ast.Inspect(file.file, file.genDecl)values = append(values, file.values...)}}g.Printf("func _() {n")g.Printf("t// An "invalid array index" compiler error signifies that the constant values have changed.n")g.Printf("t// Re-run the stringer command to generate them again.n")g.Printf("tvar x [1]struct{}n")for _, v := range values {g.Printf("t_ = x[%s - %s]n", v.originalName, v.str)}g.Printf("}n")runs := splitIntoRuns(values)switch {case len(runs) == 1:g.buildOneRun(runs, typeName)...}
}

随后调用的 Generator.buildOneRun 会生成两个常量的声明语句并为类型定义 String 方法,其中引用的 stringOneRun 常量是方法的模板,与 Web 服务的前端 HTML 模板比较相似:

func (g *Generator) buildOneRun(runs [][]Value, typeName string) {values := runs[0]g.Printf("n")g.declareIndexAndNameVar(values, typeName)g.Printf(stringOneRun, typeName, usize(len(values)), "")
}const stringOneRun = `func (i %[1]s) String() string {if %[3]si >= %[1]s(len(_%[1]s_index)-1) {return "%[1]s(" + strconv.FormatInt(int64(i), 10) + ")"}return _%[1]s_name[_%[1]s_index[i]:_%[1]s_index[i+1]]
}

整个生成代码的过程就是使用编译器提供的库解析源文件并按照已有的模板生成新的代码,这与 Web 服务中利用模板生成 HTML 文件没有太多的区别,只是最终生成的文件的用途稍微有一些不同,

2.3 小结

Go 语言的标准库中暴露了编译器的很多能力,其中包含词法分析和语法分析,我们可以直接利用这些现成的解析器编译 Go 语言的源文件并获得抽象语法树,有了识别源文件结构的能力,我们就可以根据源文件对应的抽象语法树自由地生成更多的代码,使用元编程技术来减少代码重复、提高工作效率。

全套教程点击下方链接直达:

IT实战:Go语言设计与实现自学教程​zhuanlan.zhihu.com

cmd命令跳舞代码_Golang语言元编程之代码生成相关推荐

  1. c语言代码运行成图指令代码,C语言图形编程代码.doc

    C语言图形编程代码 C语言图形编程代码 自己以前编写的C语言图形编程代码 自己以前编写的图形编程代码实现DOS下256BMP图片显示,中文注释,中文汉字显示 写的不好,给大家交流与学习,TC3.0与T ...

  2. mcem r语言代码_R语言面向对象编程:S3和R6

    R语言面向对象编程:S3和R6 2017-06-10 0 R语言面向对象编程:S3和R6 一.基于S3的面向对象编程 基于S3的面向对象编程是一种基于泛型函数(generic function)的实现 ...

  3. c语言函数除法代码,C语言高效编程与代码优化~

    原标题:C语言高效编程与代码优化~ 译文链接:http://www.codeceo.com/article/c-high-performance-coding.html 英文原文:https://ww ...

  4. 万年历c语言编程代码_C语言高效编程与代码优化~

    译文链接:http://www.codeceo.com/article/c-high-performance-coding.html 英文原文:https://www.codeproject.com/ ...

  5. golang sqlx scan 到结构体中_Golang语言并发编程之定时器

    上一章中对于golang的常用关键字说明如下: 1 for 和 range 2 select 3 defer 4 panic 和 recover 5 make 和 new 接下来我们来对golang的 ...

  6. cmd命令行代码复制

    打开电脑命令行界面,以为命令行的代码是不能复制的. 没想到右键标记,选择信息,回车就可以复制了 效果

  7. 第12章 元编程与注解、反射

    第12章 元编程与注解.反射 反射(Reflection)是在运行时获取类的函数(方法).属性.父类.接口.注解元数据.泛型信息等类的内部信息的机制.这些信息我们称之为 RTTI(Run-Time T ...

  8. python命令窗口在哪里_详解如何在cmd命令窗口中搭建简单的python开发环境

    详解如何在cmd命令窗口中搭建简单的python开发环境 1.快捷键win+r输入cmd回车调出cmd界面,在命令行输入python回车,显示python命令无法识别 2.登陆python官网http ...

  9. java元编程_一文读懂元编程

    元编程(Metaprogramming)是编写.操纵程序的程序,简而言之即为用代码生成代码.元编程是一种编程范式,在传统的编程范式中,程序运行是动态的,但程序本身是静态的.在元编程中,两者都是动态的[ ...

  10. 什么是元编程(meta-promgramming)?

    可以扩展程序自身,这样的能力,为元编程. 比如Ruby,使用元编程可以扩展出领域专用语言DSL, 比如RSpec.puppet.chef等.相对于新创造的领域专用语言DSL来说, Ruby语言就成为其 ...

最新文章

  1. 浅谈Java的输入输出流
  2. 我的angularjs源码学习之旅2——依赖注入
  3. MySQL 空间数据 简单操作
  4. 用户访问共享计算机没有权限,win7共享没有权限访问 共享文件访问权限的方法...
  5. Zookeeper和分布式环境中的假死脑裂问题(转)
  6. 【10.20校内测试】【小模拟】【无向图建树判奇偶环】【树上差分】
  7. 并发(concurrency)和并行(parallellism)
  8. REVERSE-PRACTICE-CthulhuOJ
  9. spring aop源码分析
  10. 【kafka】kerberos认证下 kafka 报错Bootstrap broker host:ip (id: -1 rack: null) disconnected
  11. Tensorflow训练神经网络
  12. [转载]CMMI之功能点估算法:EI、EQ和EO
  13. 利用VBB仿真——实现数码管色子
  14. html5妇女节游戏,2020三八妇女节趣味游戏大全_妇女节可以举办哪些活动
  15. 【格式转换】将JPEG图片批量处理为jpg格式
  16. 凌晨四点洛杉矶-致奋斗的我
  17. Stata | 时间序列操作
  18. word长篇文档排版技巧教学视频
  19. 数据恢复软件如何恢复电脑删除的文件
  20. 数据分析七种降维方法

热门文章

  1. python+ UIAutomator2+WEditor环境安装详情教学以及案例
  2. c语言编程if语句的用法,c语言if语句的用法有哪些
  3. SPSS软件入门常识
  4. 云计算10个入门基础知识
  5. php 过滤微信符号昵称,PHP处理微信昵称特殊符号过滤方法
  6. mongodb 实现transaction
  7. C# 字节(数组)与位之间的计算
  8. 以生活中的例子快速理解十个设计模式
  9. vue3 vite ant deign vue 黑暗模式实现
  10. tomcat启动报错 exception_access_violation(0x0000005) at pc=0x000000006d9f904