56星座屋
当前位置: 首页 星座百科

go web编程(GO编程元编程之代码生成)

时间:2023-07-11 作者: 小编 阅读量: 1 栏目名: 星座百科

Go语言中的测试就使用了代码生成机制,gotest命令会扫描包中的测试用例并生成程序、编译并执行它们,我们在这一节中就会介绍Go语言中的代码生成机制。总而言之,元编程其实是一种使用代码生成代码的方式,无论是编译期间生成代码,还是在运行时改变代码的行为都是『生成代码』的一种。Go语言作为编译型的编程语言,它提供了比较有限的运行时元编程能力,例如:反射特性,然而由于性能的问题,反射在很多场景下都不被推荐使用。

代码生成

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

设计原理

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

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

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

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

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

代码生成

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

//go:generate command argument...

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

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

// pill.gopackage painkiller//go:generate stringer -type=Pilltype Pill intconst (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 文件没有太多的区别,只是最终生成的文件的用途稍微有一些不同,

小结

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

    推荐阅读
  • 形成酸雨的主要气体是什么(形成酸雨的主要气体)

    以下内容大家不妨参考一二希望能帮到您!形成酸雨的主要气体是什么酸雨是指PH小于5.6的雨雪或其他形式的降水,形成的主要气体有二氧化硫、三氧化硫、硫化氢、二氧化氮。酸雨主要是人为的向大气中排放大量酸性物质所造成的。酸雨又分硝酸型酸雨和硫酸型酸雨。

  • 木棉花的花语是什么(木棉花的意义)

    接下来我们就一起去了解一下吧!珍惜眼前的幸福,珍惜身边的人给他们快乐与幸福。它的花期通常在3月或者4月份,在这一段时间盛开,而传说中四月的第十一天,是木棉花盛开的日子,所以4月11被定为木棉花的日子。

  • 炒凉皮不碎技巧(炒凉皮不碎有什么技巧)

    以下内容大家不妨参考一二希望能帮到您!炒凉皮不碎技巧炒凉皮不碎技巧:就是在做凉皮时不能炒太久,变软会失去筋度。胡萝卜切丝,蒜薹切段,葱切花,猪肉切丝,大蒜拍扁。成品凉皮一张张卷起切粗条,抖散备用。生抽,白糖,盐,鸡精,醋,胡椒粉调成汁备用。热锅倒适量食用油烧热加入大蒜,肉丝翻炒至金黄,加入胡萝卜丝和蒜薹炒熟,凉皮翻炒均匀后随即淋入调好的汁儿翻炒均匀。

  • 近几年灭绝的鱼(瞭望在长江源寻鱼)

    长江被誉为我国淡水渔业的摇篮、鱼类基因的宝库。据青海省渔业部门统计,长江流域青海段分布有土著鱼类21种。因此,严格意义上长江源的关键鱼类指的是裂腹鱼中的小头裸裂尻鱼。2019年,李伟带领团队参加长江源科考时,将小头裸裂尻鱼列为长江源鱼类研究的代表对象。2019年4月,科考小组五个人,两台车,开始了沿河寻觅之旅。“全球平均气温上升已是科学界的共识,位于青藏高原的长江源是全球气候变化的敏感区。”科考发现,江源地区

  • 鹧鸪在什么时候季节鸣叫(鹧鸪的孵化期有多长)

    鹧鸪在什么时候季节鸣叫鹧鸪一般会在繁殖季节鸣叫,繁殖期为3-6月,3-4月间开始求偶交配。求偶期间鸣叫更为频繁,常在山岩、树桩、灌木或乔木枝上鸣叫,尤以黎明和黄昏时更甚,往往是一鸟先鸣叫,其他雄鸟一起跟随,此起彼伏。鹧鸪的孵化期在21天左右,雏鸟出壳后不久即可跟随亲鸟活动。鹧鸪的繁殖期为每年的3-6月,3-4月间开始求偶交配,每窝产卵3-6枚,多时可达8枚,卵为椭圆形或梨形,颜色为淡皮黄色至黄褐色。

  • 秋天的诗词(这些都是关于秋天的诗句)

    迢迢新秋夕,亭亭月将圆《戊申岁六月中遇火》,今天小编就来说说关于秋天的诗词?《戊申岁六月中遇火》自古逢秋悲寂寥,我言秋日胜春朝。《秋词》是处红衰翠减,苒苒物华休。惟有长江水,无语东流。宋·柳永《八声甘州》落时西风时候,人共青山都瘦。《昭君怨》雨色秋来寒,风严清江爽。《酬裴侍御对雨感时见赠》秋声万户竹,寒色五陵松。唐·李颀《望秦川》秋色无远近,出门尽寒山。宋·苏轼《九日次韵王巩》

  • 广州有几种车牌(广州车牌你有吗)

    在广州的普通上班族,有房贷还想拥有一辆车,已经不容易了。但有车想让个广州牌,那更是难上加难,再加之限行,参与摇号,竞价的人是越来越多,那中标的机会更是渺茫了!截止日期是8日24时止。9月拟配置的中小客车增量指标共16313个,是这样分配的:1.以摇号方式向单位和个人配置节能车增量指标7285个,其中,单位指标100个,个人指标7185个。

  • qq注销账号有哪几个步骤(QQ将开注销帐号功能)

    1999年2月10日,一个名为OICQ、只有几百K的软件正式上线。当时,腾讯方面表示,这是QQ团队对帐号注销功能的灰度测试。网友截图出于安全考虑,也有网友表示支持有人说,QQ不推出注销服务有自己的考虑,这是为了防止用户QQ密码被他人知道后恶意注销,给用户带来无法挽回的损失。腾讯2018年第三季度财报显示,QQ智能终端月活跃账户同比增长6.9%至6.979亿。

  • 高跟鞋不合脚怎么办(穿高跟鞋不合脚怎么办)

    4、合理利用袜子,如果不喜欢垫各种鞋垫的朋友,可以穿一双船袜,再穿高跟鞋,那样既不影响穿着效果,也不影响美观,也是比较简单和实用的方法。

  • 年四旺名字打分104分 年四旺事迹

    文章目录:一、年四旺相关名字打分113二、年四旺相关名字评分115三、年四旺相关名字推荐四、年四旺相关名字大全五、其他人还看了一、年四旺相关名字打分113年灯石志明年橘纪红兵武尊道后书法孔多塞年贷款孙敬媛年立秋里蓝业珍冯景华年见朱诗词林于思冯桂年粤日林格孟昭毅年家薛邑马布鱼鲁初雪苏沫沫卜庆中年上年掌柜秦源达刘登龙严学锋国韵酒年线高成江裘梦年维泗红沙日年周王克斌王翔千毛淑红龙威信李万和年神范小慧王大