简介
今天我们介绍一个 Go 代码生成库jennifer
。jennifer
支持所有的 Go 语法和特性,可以用它来生成任何 Go 语言代码。
感谢kiyonlin的推荐!
快速使用
先安装:
$ go get github.com/dave/jennifer
今天我们换个思路来介绍jennifer
这个库。既然,它是用来生成 Go 语言代码的。我们就先写出想要生成的程序,然后看看如何使用jennifer
来生成。先从第一个程序Hello World
开始:
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}
我们如何用jennifer
来生成上面的程序代码呢:
package main
import (
"fmt"
. "github.com/dave/jennifer/jen"
)
func main() {
f := NewFile("main")
f.Func().Id("main").Params().Block(
Qual("fmt", "Println").Call(Lit("Hello, world")),
)
fmt.Printf("%#v", f)
}
Go 程序的基本组织单位是文件,每个文件属于一个包,可执行程序必须有一个main
包,main
包中又必须有一个main
函数。所以,我们使用jennifer
生成代码的大体步骤也是差不多的:
- 先使用
NewFile
定义一个包文件对象,参数即为包名; - 然后调用这个包文件对象的
Func()
定义函数; - 函数可以使用
Params()
定义参数,通过Block()
定义函数体; - 函数体是由一条一条语句组成的。语句的内容比较多样,后面会详细介绍。
上面代码中,我们首先定义一个main
包文件对象。然后调用其Func()
定义一个函数,Id()
为函数命名为main
,Params()
传入空参数,Block()
中传入函数体。函数体中,我们使用Qual("fmt", "Println")
来表示引用fmt.Println
这个函数。使用Call()
表示函数调用,然后Lit()
将字符串 "Hello World" 字面量作为参数传给fmt.Println()
。
Qual
函数这里需要特意讲一下,我们不需要显示导入包,Qual
函数的第一个参数就是包路径。如果是标准库,直接就是包名,例如这里的fmt
。如果是第三方库,需要加上包路径限定,例如github.com/spf13/viper
。这也是Qual
名字的由来(Qualified
,想到full qualified class name了吗😄)。jennifer
在生成程序时会汇总所有用到的包,统一导入。
运行程序,我们最终输出了一开始想要生成的程序代码!
实际上,大多数编程语言的语法都有相通之处。Go 语言的语法比较简单:
- 基本的概念:变量、函数、结构等,它们都有一个名字,又称为标识符。直接写在程序中的数字、字符串等被称为字面量,如上面的 "Hello World" ;
- 流程控制:条件(
if
)、循环(for
); - 函数和方法;
- 并发相关:goroutine 和 channel。
有几点注意:
- 我们在导入
jennifer
包的时候在包路径前面加了一个.
,使用这种方式导入,在后面使用该库的相关函数和变量时不需要添加jen.
限定。一般是不建议这样做的。但是jennifer
的函数比较多,如果不这样的话,每次都需要加上jen.
比较繁琐。 jennifer
的大部分方法都是可以链式调用的,每个方法处理完成之后都会返回当前对象,便于代码的编写。
下面我们从上面几个部分依次来介绍。
变量定义与运算
其实从语法层面来讲,变量就是标识符 + 类型。上面我们直接使用了字面量,这次我们想先定义一个变量存储欢迎信息:
func main() {
var greeting = "Hello World"
fmt.Println(greeting)
}
变量定义的方式有好几种,jennifer
可以比较直观的表达我们的意图。例如,上面的var greeting = "Hello World"
,我们基本上可以逐字翻译:
var
是变量定义,jennifer
中有对应的函数Var()
;greeting
实际上是一个标识符,我们可以用Id()
来定义;=
是赋值操作符,我们使用Op("=")
来表示;- "Hello World" 是一个字符串字面量,最开始的例子中已经介绍过了,可以使用
Lit()
定义。
所以,这条语句翻译过来就是:
Var().Id("greeting").Op("=").Lit("Hello World")
同样的,我们可以试试另一种变量定义方式greeting := "Hello World"
:
Id("greeting").Op(":=").Lit("Hello World")
是不是很简单。整个程序如下(省略包名和导入,下同):
func main() {
f := NewFile("main")
f.Func().Id("main").Params().Block(
// Var().Id("greeting").Op("=").Lit("Hello World"),
Id("greeting").Op(":=").Lit("Hello World"),
Qual("fmt", "Println").Call(Id("greeting")),
)
fmt.Printf("%#v\n", f)
}
接下来,我们用变量做一些运算。假设,我们要生成下面这段程序:
package main
import "fmt"
func main() {
var a = 10
var b = 2
fmt.Printf("%d + %d = %d\n", a, b, a+b)
fmt.Printf("%d + %d = %d\n", a, b, a-b)
fmt.Printf("%d + %d = %d\n", a, b, a*b)
fmt.Printf("%d + %d = %d\n", a, b, a/b)
}
变量定义这里不再赘述了,方法和函数调用实际上快速开始部分也介绍过。首先用Qual("fmt", "Printf")
表示取包fmt
中的Printf
函数这一概念。然后使用Call()
表示函数调用,参数第一个是字符串字面量,用Lit()
表示。第二个和第三个都是一个标识符,用Id("a")
和Id("b")
即可表示。最后一个参数是两个标识符之间的运算,运算用Op()
表示,所以最终就是生成程序:
func main() {
f := NewFile("main")
f.Func().Id("main").Params().Block(
Var().Id("a").Op("=").Lit(10),
Var().Id("b").Op("=").Lit(2),
Qual("fmt", "Printf").Call(Lit("%d + %d = %d\n"), Id("a"), Id("b"), Id("a").Op("+").Id("b")),
Qual("fmt", "Printf").Call(Lit("%d + %d = %d\n"), Id("a"), Id("b"), Id("a").Op("-").Id("b")),
Qual("fmt", "Printf").Call(Lit("%d + %d = %d\n"), Id("a"), Id("b"), Id("a").Op("*").Id("b")),
Qual("fmt", "Printf").Call(Lit("%d + %d = %d\n"), Id("a"), Id("b"), Id("a").Op("/").Id("b")),
)
fmt.Printf("%#v\n", f)
}
逻辑运算是类似的。
条件和循环
假设我们要生成下面的程序:
func main() {
score := 70
if score >= 90 {
fmt.Println("优秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")
}
}
依然采取我们的逐字翻译大法:
if
关键字用If()
来表示,条件语句是基本的标识符和常量操作。条件语句块与函数体一样,都使用Block()
;else
关键字用Else()
来表示,else if
就是Else()
后面再调用If()
即可。
完整的代码如下:
func main() {
f := NewFile("main")
f.Func().Id("main").Params().Block(
Id("score").Op(":=").Lit(70),
If(Id("score").Op(">=").Lit(90)).Block(
Qual("fmt", "Println").Call(Lit("优秀")),
).Else().If(Id("score").Op(">=").Lit(80)).Block(
Qual("fmt", "Println").Call(Lit("良好")),
).Else().If(Id("score").Op(">=").Lit(60)).Block(
Qual("fmt", "Println").Call(Lit("及格")),
).Else().Block(
Qual("fmt", "Println").Call(Lit("不及格")),
),
)
fmt.Printf("%#v\n", f)
}
对于for
循环也是类似的,如果我们要生成下面的程序:
package main
import "fmt"
func main() {
var sum int
for i := 1; i