Go 模板:用代碼生成代碼

用代碼生成代碼。

不用 Go 寫代碼,就不知道 Java 程序員被“慣”得有多厲害。 Java 奉行“拿來主義”,什麼東西都有現成的庫。而 Go 就沒有那麼豐富的庫了。

本文用生成器模式作爲例子,來演示如何用代碼生成代碼。

生成器模式

熟悉 Java 開發的同學都知道,lombok 有一個著名的註解 @Builder ,只要加在類上面,就可以自動生成 Builder 模式的代碼。如下所示:

@Builder
public class DetectionQuery {

    private String uniqueKey;
    private Long   startTime;
    private Long   endTime;
}

然後就可以這樣使用:

return DetectionQuery.builder()
                .uniqueKey(uniqueKey)
                .startTime(startTime)
                .endTime(endTime)
                .build();

是不是很爽?

不過 Go 可沒有這樣好用的註解。 Go 你得自己手寫。假設我們要造一輛車,車有車身、引擎、座位、輪子。Go 的生成器模式的代碼是這樣子的:

package model

import "fmt"

type ChinaCar struct {
	Body   string
	Engine string
	Seats  []string
	Wheels []string
}

func newChinaCar(body string, engine string, seats []string, wheels []string) *ChinaCar {
	return &ChinaCar{
		Body:   body,
		Engine: engine,
		Seats:  seats,
		Wheels: wheels,
	}
}

type CarBuilder struct {
	body   string
	engine string
	seats  []string
	wheels []string
}

func ChinaCharBuilder() *CarBuilder {
	return &CarBuilder{}
}

func (b *CarBuilder) Build() *ChinaCar {
	return newChinaCar(b.body, b.engine, b.seats, b.wheels)
}

func (b *CarBuilder) Body(body string) *CarBuilder {
	b.body = body
	return b
}

func (b *CarBuilder) Engine(engine string) *CarBuilder {
	b.engine = engine
	return b
}

func (b *CarBuilder) Seats(seats []string) *CarBuilder {
	b.seats = seats
	return b
}

func (b *CarBuilder) Wheels(wheels []string) *CarBuilder {
	b.wheels = wheels
	return b
}

func main() {
	car := ChinaCharBuilder().
		Body("More advanced").
		Engine("Progressed").
		Seats([]string{"good", "nice"}).
		Wheels([]string{"solid", "smooth"}).
		Build()
	fmt.Printf("%+v", car)
}

生成器模式怎麼寫?遵循三步即可:

(1) 先構造一個對應的生成器,這個生成器與目標對象有一樣的屬性;

(2) 對於每一個屬性,有一個方法設置屬性,然後返回生成器的引用本身;

(3) 最後調用生成器的 Build 方法,這個方法會調用目標對象的構造器來生成目標對象。

爲啥不直接調用目標對象的構造器,要這麼拐彎抹角呢?因爲生成器模式一般用於複雜對象的構造。這個複雜對象的每一個組件都需要逐步構造,而不是一下子都有的。必須等所有組件都正確構造完成後,才能返回一個可用的目標對象。像 CarBuilder 這種纔算是生成器模式的合理使用。而 DetectionQuery 的 builder 模式只是爲了享受鏈式調用的暢感。

用 AI 生成生成器模式代碼

啊!難道來一個 query 就得寫這麼一大堆嗎? 懶精又發作了。

對了,可以用 AI ,看看 AI 怎麼寫。

向 AI 提問:

我有個對象 ChinaCar 如下所示, 請生成對應的生成器模式的代碼 
type ChinaCar struct {
  Body   string
  Engine string
  Seats  []string
  Wheels []string
}

刷刷刷,AI 就寫出來了(這裏使用的是阿里的通義千問):

package main

import (
	"fmt"
)

type ChinaCar struct {
	Body   string
	Engine string
	Seats  []string
	Wheels []string
}

type ChinaCarBuilder struct {
	body   string
	engine string
	seats  []string
	wheels []string
}

func (c *ChinaCarBuilder) SetBody(body string) *ChinaCarBuilder {
	c.body = body
	return c
}

func (c *ChinaCarBuilder) SetEngine(engine string) *ChinaCarBuilder {
	c.engine = engine
	return c
}

func (c *ChinaCarBuilder) SetSeats(seats []string) *ChinaCarBuilder {
	c.seats = seats
	return c
}

func (c *ChinaCarBuilder) SetWheels(wheels []string) *ChinaCarBuilder {
	c.wheels = wheels
	return c
}

func (c *ChinaCarBuilder) Build() *ChinaCar {
	return &ChinaCar{
		Body:   c.body,
		Engine: c.engine,
		Seats:  c.seats,
		Wheels: c.wheels,
	}
}

func main() {
	builder := new(ChinaCarBuilder)
	builder.SetBody("Sedan").
		SetEngine("Gasoline").
		SetSeats([]string{"Driver", "Passenger"}).
		SetWheels([]string{"Front left", "Front right", "Back left", "Back right"})

	chinaCar := builder.Build()
	fmt.Println(chinaCar)
}

嗯,看上去基本一樣,只是設置屬性的方法名不一樣。有的會直接用屬性名,有的會用 With+屬性名,這裏用的是 Set+屬性名。

好耶!可是,如果還有其它模板型的代碼怎麼辦呢?還是自己親自來生成代碼吧!

生成器代碼生成

用代碼生成代碼?嗯,其實不算稀奇。代碼也只是一種普通的可讀文本而已。

模板是用於動態生成文本的常用技術。雖然看上去不算特別高明的方式,但也很管用。咱們使用 Go template 來實現它。

思路與實現

首先要分析,哪些是固定的文本,哪些是動態的文本。

紅框框出來的都是動態文本。事實上,除了幾個關鍵字和括號是靜態的以外,其它基本都是動態生成的。這些文本通常是根據業務對象類型和業務對象的屬性名及屬性類型來推理出來的。我們把這些動態文本用僞標籤語言先標出來。

先根據最終要生成的代碼,把模板文件給定義出來(這裏可以用自然語言先填充,再替換成技術實現):


func New{{ 目標對象類型 }}(逗號分隔的屬性名 屬性類型列表)) *{{ 目標對象類型 }} {
	return &{{ 目標對象類型 }}{
         每一行都是:  屬性名 :屬性名 (屬性名小寫)
	}
}

type {{ 生成器類型 }} struct {

    每一行都是:  屬性名  屬性類型(屬性名小寫)
}

func {{ 生成器方法名 }}() *{{ 生成器類型 }} {
    return &{{ 生成器類型 }}{
    }
}

func (b *{{ 生成器類型 }}) Build() *{{ 目標對象類型 }} {
    return New{{ 目標對象類型 }}(
       逗號分隔的  b.屬性名 列表
}

對於每一個屬性,遍歷,做如下動作:

func (b *{{ 生成器類型 }}) {{ 屬性名 }}({{ 屬性名(小寫) }} {{ 屬性類型 }}) *{{ 生成器類型 }} {
    b.{{ 屬性名(小寫) }} = {{ 屬性名(小寫) }}
    return b
}

然後,抽象出用來填充動態文本的填充對象:

type BuilderInfo struct {
	BuilderMethod string
	BuilderClass  string
	BizClass      string
	Attrs         []Attr
}

type Attr struct {
	Name string
	Type string
}

func newAttr(Name, Type string) Attr {
	return Attr{Name: Name, Type: Type}
}

接下來,要根據具體的模板語言,來填充上面的自然語言,同時從目標對象中生成填充對象,來填充這些動態文本和自定義函數。

如下代碼所示:

builder_tpl 就是生成器模式的代碼模板文本。我們先用具體的值填充,把模板調通,然後再把這些具體的值用函數替換。

func LowercaseFirst(s string) string {
	r, n := utf8.DecodeRuneInString(s)
	return string(unicode.ToLower(r)) + s[n:]
}

func MapName(attrs []Attr) []string {
	return util.Map[Attr, string](attrs, func(attr Attr) string { return "b." + LowercaseFirst(attr.Name) })
}

func MapNameAndType(attrs []Attr) []string {
	return util.Map[Attr, string](attrs, func(attr Attr) string { return LowercaseFirst(attr.Name) + " " + LowercaseFirst(attr.Type) })
}

func autoGenBuilder(builder_tpl string) {

	t1 := template.Must(template.New("test").Funcs(template.FuncMap{
		"lowercaseFirst": LowercaseFirst, "join": strings.Join, "mapName": MapName, "mapNameAndType": MapNameAndType,
	}).Parse(builder_tpl))
	bi := BuilderInfo{
		BuilderMethod: "QueryBuilder",
		BuilderClass:  "CarBuilder",
		BizClass:      "ChinaCar",
		Attrs: []Attr{
			newAttr("Body", "string"), newAttr("Engine", "string"),
			newAttr("Seats", "[]string"), newAttr("Wheels", "[]string")},
	}
	t1.ExecuteTemplate(os.Stdout, "test", bi)
}

func main() {

	builder_tpl := `

func New{{ .BizClass }}({{- join (mapNameAndType .Attrs) ", " }})) *{{ .BizClass }} {
	return &{{ .BizClass }}{
    {{ range .Attrs }}
		{{ .Name }}:      {{ lowercaseFirst .Name }},
    {{ end }}
	}
}

type {{ .BuilderClass }} struct {

{{ range .Attrs }}
    {{ lowercaseFirst .Name }}   {{ .Type }}
{{ end }}
}

func {{ .BuilderMethod }}() *{{ .BuilderClass }} {
    return &{{ .BuilderClass }}{
    }
}

func (b *{{ .BuilderClass }}) Build() *{{ .BizClass }} {
    return New{{ .BizClass }}(
       {{- join (mapName .Attrs) ", " }})
}

{{- range .Attrs }}
func (b *{{ $.BuilderClass }}) {{ .Name }}({{ lowercaseFirst .Name }} {{ .Type }}) *{{ $.BuilderClass }} {
    b.{{ lowercaseFirst .Name }} = {{ lowercaseFirst .Name }}
    return b
}
{{- end }}

`
	car := model.ChinaCar{}
	//autoGenBuilder(builder_tpl)

	autoGenBuilder2(builder_tpl, car)
}

Go Template 語法

這裏基本上概括了Go template 的常用語法:

  • {{ . }} 表示頂層作用域對象,也就是你從如下方法傳入的 bi 對象。
  • {{ .BuilderClass }} 就是取 bi.BuilderClass , {{ .Attrs }} 就是取 bi.Attrs
t1.ExecuteTemplate(os.Stdout, "test", bi)
  • 這有個 range 循環, 取 Attrs 裏的每一個元素進行循環。注意到,range 裏面的 {{ .Name }} 的 . 表示的是 Attrs 裏的每一個元素對象。
    {{ range .Attrs }}
		{{ .Name }}:      {{ lowercaseFirst .Name }},
    {{ end }}
  • 這裏還傳入了一個自定義函數 lowercaseFirst, 可以通過如下方法傳入:
t1 := template.Must(template.New("test").Funcs(template.FuncMap{
		"lowercaseFirst": LowercaseFirst, "join": strings.Join, "mapName": MapName, "mapNameAndType": MapNameAndType,
	}).Parse(builder_tpl))
  • 還有一個技巧,就是如何在 range 循環裏引用頂層對象。這裏要引用 BuilderClass 的值,必須用 $.BuilderClass,否則輸出爲空。
{{- range .Attrs }}
func (b *{{ $.BuilderClass }}) {{ .Name }}({{ lowercaseFirst .Name }} {{ .Type }}) *{{ $.BuilderClass }} {
    b.{{ lowercaseFirst .Name }} = {{ lowercaseFirst .Name }}
    return b
}
{{- end }}

嗯,多寫寫就熟了。通過實戰來練習和掌握是一種高效學習之法。

注意一定要寫 . 號。 我最開始老是忘記寫。然後就卡住沒響應了。

go template 報錯不太友好。分三種情況:

  • 直接卡住,你也不知道到底發生了什麼。比如 {{ .BuilderClass }} 寫成 {{ BuilderClass }}
  • 直接報錯,地址引用錯誤。 比如模板語法錯誤。
  • 不輸出內容。比如引用不到內容。

進一步完善

接下來,就要把寫死的 BuilderMethod, BuilderClass, BizClass 和 Attrs 通過給定的業務類型來生成。這不難辦,問 AI 就可以了:

func GetBizClass(t any) string {
	qualifiedClass := fmt.Sprintf("%T", t)
	return qualifiedClass[strings.Index(qualifiedClass, ".")+1:]
}

func GetAttributes(obj any) []Attr {
	typ := reflect.TypeOf(obj)
	attrs := make([]Attr, typ.NumField())

	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		attr := Attr{
			Name: field.Name,
			Type: field.Type.String(),
		}
		attrs[i] = attr
	}

	return attrs
}

用 GetBizClass 和 GetAttributes 生成的值分別填充那幾處硬寫的值即可。

程序的主體,本文已經都給出來了,讀者也可以將其拼接起來,做一次完型填空。

AI 輔助編程

這次, AI 可幫了忙。我也是剛上手 Go 編程語言不久,對 Go 的語法和庫的掌握比較生疏,因此逢疑就問 AI。

這說明:當一個人能夠熟練使用某種語言進行開發時,如果要切換到一種新語言上, AI 能夠給予很大的幫助,快速掃清障礙,熟悉新語言和新庫。

試想,如果沒有 AI, 我還得去網上去查 template 的語法,出了錯也不知道是怎麼回事,這個挺耗費時間和情緒的。

小結

嗯,雖然是做了個 demo,但是要做成完善的成品給別人用,還是有很多改善之處的,比如健壯性、更多的屬性類型。此外,其它的代碼生成技術也可以去了解下。

相關文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章