Go 每日一庫之 jennifer

熱烈歡迎你,相識是一種緣分,Echa 哥為了你的到來特意準備了一份驚喜,go學習資料《 》


Go 每日一庫之 jennifer


今天我們介紹一個 Go 代碼生成庫jennifer。jennifer支持所有的 Go 語法和特性,可以用它來生成任何 Go 語言代碼。

感謝kiyonlin的推薦!

快速使用

先安裝:

<code>$ go get github.com/dave/jennifer
複製代碼/<code>

今天我們換個思路來介紹jennifer這個庫。既然,它是用來生成 Go 語言代碼的。我們就先寫出想要生成的程序,然後看看如何使用jennifer來生成。先從第一個程序Hello World開始:

<code>package main

import "fmt"

func main() {
fmt.Println("Hello World")
}
複製代碼/<code>

我們如何用jennifer來生成上面的程序代碼呢:

<code>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)
}
複製代碼/<code>


Go 每日一庫之 jennifer


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的大部分方法都是可以
    鏈式調用的,每個方法處理完成之後都會返回當前對象,便於代碼的編寫。

下面我們從上面幾個部分依次來介紹。

變量定義與運算

其實從語法層面來講,變量就是標識符 + 類型。上面我們直接使用了字面量,這次我們想先定義一個變量存儲歡迎信息:

<code>func main() {
var greeting = "Hello World"
fmt.Println(greeting)
}
複製代碼/<code>

變量定義的方式有好幾種,jennifer可以比較直觀的表達我們的意圖。例如,上面的var greeting = "Hello World",我們基本上可以逐字翻譯:

  • var是變量定義,jennifer中有對應的函數Var();
  • greeting實際上是一個標識符,我們可以用Id()來定義;
  • =是賦值操作符,我們使用Op("=")來表示;
  • **"Hello World"**是一個字符串字面量,最開始的例子中已經介紹過了,可以使用Lit()定義。

所以,這條語句翻譯過來就是:

<code>Var().Id("greeting").Op("=").Lit("Hello World")
複製代碼/<code>

同樣的,我們可以試試另一種變量定義方式greeting := "Hello World":

<code>Id("greeting").Op(":=").Lit("Hello World")
複製代碼/<code>

是不是很簡單。整個程序如下(省略包名和導入,下同):

<code>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)
}
複製代碼/<code>

接下來,我們用變量做一些運算。假設,我們要生成下面這段程序:

<code>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)
}
複製代碼/<code>

變量定義這裡不再贅述了,方法和函數調用實際上快速開始部分也介紹過。首先用Qual("fmt", "Printf")表示取包fmt中的Printf函數這一概念。然後使用Call()表示函數調用,參數第一個是字符串字面量,用Lit()表示。第二個和第三個都是一個標識符,用Id("a")和Id("b")即可表示。最後一個參數是兩個標識符之間的運算,運算用Op()表示,所以最終就是生成程序:

<code>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)
}
複製代碼/<code>

邏輯運算是類似的。

條件和循環

假設我們要生成下面的程序:

<code>func main() {
score := 70

if score >= 90 {
fmt.Println("優秀")
} else if score >= 80 {
fmt.Println("良好")
} else if score >= 60 {
fmt.Println("及格")
} else {
fmt.Println("不及格")

}
}
複製代碼/<code>

依然採取我們的逐字翻譯大法:

  • if關鍵字用If()來表示,條件語句是基本的標識符和常量操作。條件語句塊與函數體一樣,都使用Block();
  • else關鍵字用Else()來表示,else if就是Else()後面再調用If()即可。

完整的代碼如下:

<code>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)
}
複製代碼/<code>

對於for循環也是類似的,如果我們要生成下面的程序:

<code>package main

import "fmt"

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

fmt.Println(sum)
}
複製代碼/<code>

我們需要編寫下面的程序:

<code>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("%#v\\n", f)
}
複製代碼/<code>

For()裡面的 3 條語句對應實際for語句中的 3 個部分。

函數

函數是每個編程語言的必要元素。函數的核心要素是名字(標識符)、參數列表、返回值,最關鍵的就是函數體。我們之前編寫main函數的時候大概介紹過。假設我們要編寫一個計算兩個數的和的函數:

<code>func add(a, b int) int {
return a + b
}
複製代碼/<code>
  • 函數我們使用Func()表示,參數用Params()表示,返回值使用Int()表示;
  • 函數體用Block();
  • return語句使用Return()函數表示,其他都是一樣的。

看下面的完整代碼:

<code>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("%#v\\n", f)
}
複製代碼/<code>

一定要注意,即使沒有參數,Params()也一定要調用,否則生成的代碼有語法錯誤

結構和方法

下面我們看看結構和方法如何生成,假設我們想生成下面的程序:

<code>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()
}
複製代碼/<code>

需要用到的新函數:

  • 結構體是一個類型,所以需要用到類型定義函數Type(),然後結構體的字段在Struct()內通過Id()+類型定義;
  • 方法其實也是一個函數,只不過多了一個接收器,我們還是使用Func()定義,接收者也可以用定義參數的Params()函數來指定,其它與函數沒什麼不同;
  • 然後結構體初始化,在Values()中給字段賦值;
  • 方法先用Dot("方法名")找到方法,然後Call()調用。

最後的程序:

<code>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("%#v\\n", f)
}
複製代碼/<code>

併發支持

還是一樣,假設我想生成下面的程序:

<code>package main

import "fmt"

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

return out
}

func double(in out := make(chan int)

go func() {
for i := range in {
out }
close(out)
}()


return out
}

func main() {
for i := range double(generate()) {
fmt.Println(i)
}
}
複製代碼/<code>

需要用到的新函數:

  • 首先是make一個chan,用Make(Chan().Int());
  • 然後啟動一個 goroutine,用Go();
  • 關閉chan,用Close();
  • for ... range對應使用Range()。

拼在一起就是這樣:

<code>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(" Close(Id("out")),
).Call(),
Return().Id("out"),
)

f.Func().Id("double").Params(Id("in").Op(" Id("out").Op(":=").Make(Chan().Int()),
Go().Func().Params().Block(
For().Id("i").Op(":=").Range().Id("in").Block(Id("out").Op(" 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("%#v\\n", f)
}
複製代碼/<code>

保存代碼

上面的程序中,我們生成代碼後直接輸出了。在實際應用中,肯定是需要保存到文件中,然後編譯運行的。jennifer也提供了保存到文件的方法File.Save(),直接傳入文件名即可,這個File就是我們上面調用NewFile()生成的對象:

<code>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)
}
}

複製代碼/<code>

這種方式必須要保證generated目錄存在。所以,我們使用os庫在目錄不存在時創建一個。

常見問題

jennifer在生成代碼後會調用go fmt對代碼進行格式化,如果代碼存在語法錯誤,這時候會輸出錯誤信息。我遇到最多的問題就是最後生成的程序代碼以})結尾,這明顯不符合語法。查了半天發現Func()後忘記加Params()。即使是空參數,這個Params()也不能省略!


Go 每日一庫之 jennifer


總結

jennifer支持的遠不止我上面介紹的那些,實際上 Go 的語法和特性它都支持,如select/goto/panic/recover/continue/break,還有類型斷言b, ok := i.(bool)、註釋、cgo 等等等等。感興趣可以自己去探索。雖然jennifer函數眾多,但是按照我們的逐字翻譯大法,實際上用起來很簡單。說實話,我在寫上次的測試程序時,基本上沒看文檔,先按照自己的理解去寫,結果大部分都是對!只有一些有問題的地方再去看文檔。

說了這麼多,jennifer的用途是什麼呢?答曰,根據配置生成代碼,編寫生成代碼的工具。另外jennifer的 GitHub 中有一個genjen目錄,實際上我們用到的很多函數都是通過它來生成的,是不是很棒。


分享到:


相關文章: