首页 > 其他分享 >Go 模板:用代码生成代码

Go 模板:用代码生成代码

时间:2023-12-09 09:22:06浏览次数:38  
标签:代码生成 return string 生成器 BuilderClass func Go 模板 Name

用代码生成代码。

不用 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 来实现它。

思路与实现

首先要分析,哪些是固定的文本,哪些是动态的文本。

红框框出来的都是动态文本。事实上,除了几个关键字和括号是静态的以外,其它基本都是动态生成的。这些文本通常是根据业务对象类型和业务对象的属性名及属性类型来推理出来的。我们把这些动态文本用伪标签语言先标出来。

这里要抽象出用来填充动态文本的对象:

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,但是要做成完善的成品给别人用,还是有很多改善之处的,比如健壮性、更多的属性类型。此外,其它的代码生成技术也可以去了解下。

相关文章

标签:代码生成,return,string,生成器,BuilderClass,func,Go,模板,Name
From: https://www.cnblogs.com/lovesqcc/p/17889473.html

相关文章

  • vscode-go语言插件,分析(三)
    goDebugConfiguration.ts配置GoDebugConfigurationProvider实现vscode.DebugConfigurationProvider接口goDebugFactory.ts调试工厂GoDebugAdapterDescriptorFactory描述工厂,实现vscode.DebugAdapterDescriptorFactory接口GoDebugAdapterTrackerFactory跟踪器,能够读取记录......
  • 极客时间邓明初级go工程师训练营
    获取完整版--》请留言1.变量变量的声明有四种方式:声明一个变量,默认的初始化值为0:varaint声明一个变量,初始值为100:varaint=100初始化时候省略数据类型,通过值自动推导变量的数据类型:vara=100省略掉var关键字,直接自动匹配,但要使用:=a:=100一个注意的点:第四种声明变量的方......
  • django如何远程查询多对多字段?
    解决办法假设A表有一个多对多properties字段,通过A表远程查询properties的名称qs=A.objects.filter(properties__name=xxx)更复杂的:A表有一个外键字段version连接B表,B表有一个多对多字段properties,通过A表远程查询properties的名称qs=A.objects.filter(version__proper......
  • excel导出模板,导入数据 后端代码
    依赖如下<!--poi3.9:导出excel--> <dependency>  <groupId>org.apache.poi</groupId>  <artifactId>poi</artifactId>  <version>3.9</version> </dependency> <dependency>  <groupId>org.apac......
  • 私域运营:12个朋友圈经营模板
    做私域运营的各位,想必大家都会烦恼朋友圈要发什么才能保证最高效吧!首先,我们需要明确,朋友圈是什么?朋友圈是我们打造信任感的地方,也是我们的信息能够及时触达用户的重要渠道。很多人都有一个习惯,每当添加一个新好友微信后会去翻一下ta的朋友圈。因为除了添加后说的第一句话,我们要了解......
  • 使用django连接MySQL
    使用python311+pycharm社区版+MySQL80,使用django连接MySQL1.下载1.1.官网下载pythonhttps://www.python.org/downloads/如我的python3.11.4,可以进入如下的页面进行hash校验https://www.python.org/downloads/release/python-3114/1.2.官网下载pycharmhttps://www.......
  • 文件权限UGO
    一、基本权限UGOLinux系统通过U、G、O将用户分为三类,并对这三类用户分别设置三种基本权限,这种设置权限的方式称作UGO方式1、设置文件属性与权限chown:修改文件属主、属组chgrp:修改文件属组chmod:修改文件权限若要将某目录下的所以子目录或文件同时修改属主或属组,只需要在chown......
  • E. Good Triples
    绝,太绝了看我娓娓道来1.如果\(a+b+c\)过程中有进位,那么位数和肯定不等(+1-10)2.由此可知,只要相加过程中没有进位的abc就是合法的3.n的每一位等于abc对应的每一位的和4.最后一步就是排列组合的思维,我真的词穷了。。。代码#include<bits/stdc++.h>usingnamespacestd;#defin......
  • 使用freemarker,导出制作好的ftl模板,并写入数据
    使用freemarker,导出制作好的ftl模板,并写入数据一、背景1.1项目背景最近在开发一个项目,需要导出一些数据,然后写入到word文档中,然后再导出到本地,这个需求是比较常见的,但是我在网上找了很多资料,都没有找到一个比较好的解决方案,所以就自己写了一个,这里分享给大家,希望能帮助到大家......
  • 如何使用gogs搭建自己的git服务器
    最近偶然发现一款轻量级的git服务器,以前一直用的svnserver,最近想搞个git服务器,用gitlab资源占用太多了,gogs是一款轻量级git服务器,非常适合个人使用。 项目地址:https://github.com/gogs/gogs  以下教程是基于Windows/WindowsServer系统搭建步骤1、安装先决条件数据......