Go 每日一库之 jennifer

幸运草 2020年4月12日22:10:09前端框架评论阅读模式

今天我们介绍一个 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("%#vn", f)
}

接下来,我们用变量做一些运算。假设,我们要生成下面这段程序:

package main

import "fmt"

func main() {
var a = 10
var b = 2
fmt.Printf("%d + %d = %dn", a, b, a+b)
fmt.Printf("%d + %d = %dn", a, b, a-b)
fmt.Printf("%d + %d = %dn", a, b, a*b)
fmt.Printf("%d + %d = %dn", 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 = %dn"), Id("a"), Id("b"), Id("a").Op("+").Id("b")),
Qual("fmt", "Printf").Call(Lit("%d + %d = %dn"), Id("a"), Id("b"), Id("a").Op("-").Id("b")),
Qual("fmt", "Printf").Call(Lit("%d + %d = %dn"), Id("a"), Id("b"), Id("a").Op("*").Id("b")),
Qual("fmt", "Printf").Call(Lit("%d + %d = %dn"), Id("a"), Id("b"), Id("a").Op("/").Id("b")),
)
fmt.Printf("%#vn", 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("%#vn", f)
}

对于for循环也是类似的,如果我们要生成下面的程序:

package main

import "fmt"

func main() {
var sum int
for i := 1; i <= 100; i++ {
sum += i
}

fmt.Println(sum)
}

我们需要编写下面的程序:

func main() {
f := NewFile("main")

f.Func().Id("main").Params().Block(
Var().Id("sum").Int(),

For(
Id("i").Op(":=").Lit(0),
Id("i").Op("<=").Lit(100),
Id("i").Op("++"),
).Block(
Id("sum").Op("+=").Id("i"),
),

Qual("fmt", "Println").Call(Id("sum")),
)

fmt.Printf("%#vn", f)
}

For()里面的 3 条语句对应实际for语句中的 3 个部分。

函数

函数是每个编程语言的必要元素。函数的核心要素是名字(标识符)、参数列表、返回值,最关键的就是函数体。我们之前编写main函数的时候大概介绍过。假设我们要编写一个计算两个数的和的函数:

func add(a, b int) int {
return a + b
}

  • 函数我们使用Func()表示,参数用Params()表示,返回值使用Int()表示;
  • 函数体用Block();
  • return语句使用Return()函数表示,其他都是一样的。

看下面的完整代码:

func main() {
f := NewFile("main")

f.Func().Id("add").Params(Id("a"), Id("b").Int()).Int().Block(
Return(Id("a").Op("+").Id("b")),
)

f.Func().Id("main").Params().Block(
Id("a").Op(":=").Lit(1),
Id("b").Op(":=").Lit(2),
Qual("fmt", "Println").Call(Id("add").Call(Id("a"), Id("b"))),
)

fmt.Printf("%#vn", f)
}

一定要注意,即使没有参数,Params()也一定要调用,否则生成的代码有语法错误。

结构和方法

下面我们看看结构和方法如何生成,假设我们想生成下面的程序:

package main

import "fmt"

type User struct {
Name string
Age int
}

func (u *User) Greeting() {
fmt.Printf("Hello %s", u.Name)
}
func main() {
u := User{Name: "dj", Age: 18}
u.Greeting()
}

需要用到的新函数:

  • 结构体是一个类型,所以需要用到类型定义函数Type(),然后结构体的字段在Struct()内通过Id()+类型定义;
  • 方法其实也是一个函数,只不过多了一个接收器,我们还是使用Func()定义,接收者也可以用定义参数的Params()函数来指定,其它与函数没什么不同;
  • 然后结构体初始化,在Values()中给字段赋值;
  • 方法先用Dot("方法名")找到方法,然后Call()调用。

最后的程序:

func main() {
f := NewFile("main")

f.Type().Id("User").Struct(
Id("Name").String(),
Id("Age").Int(),
)

f.Func().Params(Id("u").Id("*User")).Id("Greeting").Params().Block(
Qual("fmt", "Printf").Call(Lit("Hello %s"), Id("u").Dot("Name")),
)

f.Func().Id("main").Params().Block(
Id("u").Op(":=").Id("User").Values(
Id("Name").Op(":").Lit("dj"),
Id("Age").Op(":").Lit(18),
),
Id("u").Dot("Greeting").Call(),
)

fmt.Printf("%#vn", f)
}

并发支持

还是一样,假设我想生成下面的程序:

package main

import "fmt"

func generate() chan int {
out := make(chan int)
go func() {
for i := 1; i <= 100; i++ {
out <- i
}
close(out)
}()

return out
}

func double(in <-chan int) chan int {
out := make(chan int)

go func() {
for i := range in {
out <- i * 2
}
close(out)
}()

return out
}

func main() {
for i := range double(generate()) {
fmt.Println(i)
}
}

需要用到的新函数:

  • 首先是make一个chan,用Make(Chan().Int());
  • 然后启动一个 goroutine,用Go();
  • 关闭chan,用Close();
  • for ... range对应使用Range()。

拼在一起就是这样:

func main() {
f := NewFile("main")
f.Func().Id("generate").Params().Chan().Int().Block(
Id("out").Op(":=").Make(Chan().Int()),
Go().Func().Params().Block(
For(
Id("i").Op(":=").Lit(1),
Id("i").Op("<=").Lit(100),
Id("i").Op("++"),
).Block(Id("out").Op("<-").Id("i")),
Close(Id("out")),
).Call(),
Return().Id("out"),
)

f.Func().Id("double").Params(Id("in").Op("<-").Chan().Int()).Chan().Int().Block(
Id("out").Op(":=").Make(Chan().Int()),
Go().Func().Params().Block(
For().Id("i").Op(":=").Range().Id("in").Block(Id("out").Op("<-").Id("i").Op("*").Lit(2)),
Close(Id("out")),
).Call(),
Return().Id("out"),
)

f.Func().Id("main").Params().Block(
For(
Id("i").Op(":=").Range().Id("double").Call(Id("generate").Call()),
).Block(
Qual("fmt", "Println").Call(Id("i")),
),
)

fmt.Printf("%#vn", f)
}

保存代码

上面的程序中,我们生成代码后直接输出了。在实际应用中,肯定是需要保存到文件中,然后编译运行的。jennifer也提供了保存到文件的方法File.Save(),直接传入文件名即可,这个File就是我们上面调用NewFile()生成的对象:

func main() {
f := NewFile("main")
f.Func().Id("main").Params().Block(
Qual("fmt", "Println").Call(Lit("Hello, world")),
)

_, err := os.Stat("./generated")
if os.IsNotExist(err) {
os.Mkdir("./generated", 0666)
}

err = f.Save("./generated/main.go")
if err != nil {
log.Fatal(err)
}
}

这种方式必须要保证generated目录存在。所以,我们使用os库在目录不存在时创建一个。

常见问题

jennifer在生成代码后会调用go fmt对代码进行格式化,如果代码存在语法错误,这时候会输出错误信息。我遇到最多的问题就是最后生成的程序代码以})结尾,这明显不符合语法。查了半天发现Func()后忘记加Params()。即使是空参数,这个Params()也不能省略!

总结

jennifer支持的远不止我上面介绍的那些,实际上 Go 的语法和特性它都支持,如select/goto/panic/recover/continue/break,还有类型断言b, ok := i.(bool)、注释、cgo 等等等等。感兴趣可以自己去探索。虽然jennifer函数众多,但是按照我们的逐字翻译大法,实际上用起来很简单。说实话,我在写上次的测试程序时,基本上没看文档,先按照自己的理解去写,结果大部分都是对!只有一些有问题的地方再去看文档。

说了这么多,jennifer的用途是什么呢?答曰,根据配置生成代码,编写生成代码的工具。另外jennifer的 GitHub 中有一个genjen目录,实际上我们用到的很多函数都是通过它来生成的,是不是很棒。

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
幸运草
Go语言接口规则 前端框架

Go语言接口规则

Go语言接口规则 接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口对应的全部方法签名。就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。对应方法,是指有相同名称、参数...
Go语言中处理 HTTP 服务器 前端框架

Go语言中处理 HTTP 服务器

1 概述 包 net/http 提供了HTTP服务器端和客户端的实现。本文说明关于服务器端的部分。 快速开始: package main import (   "log"   "net/http" )...

发表评论