124. Go Template应用实例:用代码生成代码
文章目录
- 生成器模式
- 生成器代码生成
本文用生成器模式作为例子,来演示如何用代码生成代码。
生成器模式
熟悉 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 modelimport "fmt"type ChinaCar struct {Body stringEngine stringSeats []stringWheels []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 stringengine stringseats []stringwheels []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 = bodyreturn b
}func (b *CarBuilder) Engine(engine string) *CarBuilder {b.engine = enginereturn b
}func (b *CarBuilder) Seats(seats []string) *CarBuilder {b.seats = seatsreturn b
}func (b *CarBuilder) Wheels(wheels []string) *CarBuilder {b.wheels = wheelsreturn 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 方法,这个方法会调用目标对象的构造器来生成目标对象。
为啥不直接调用目标对象的构造器,要这么拐弯抹角呢?因为生成器模式一般用于复杂对象的构造。这个复杂对象的每一个组件都需要逐步构造,比如每一个Set方法中,可能都有一些对于属性的校验或者其他业务逻辑,而不是简单的给属性赋值就行。必须等所有组件都正确构造完成后,才能返回一个可用的目标对象。像 CarBuilder 这种才算是生成器模式的合理使用。而 DetectionQuery 的 builder 模式只是为了享受链式调用的畅感。
生成器代码生成
用代码生成代码?嗯,其实不算稀奇。代码也只是一种普通的可读文本而已。
模板是用于动态生成文本的常用技术。虽然看上去不算特别高明的方式,但也很管用。咱们使用 Go template 来实现它。
思路与实现
首先要分析,哪些是固定的文本,哪些是动态的文本。
红框框出来的都是动态文本。事实上,除了几个关键字和括号是静态的以外,其它基本都是动态生成的。这些文本通常是根据业务对象类型和业务对象的属性名及属性类型来推理出来的。

先根据最终要生成的代码,把模板文件给定义出来(这里可以用自然语言先填充,再替换成技术实现):
func New{{ 目标对象类型 }}(逗号分隔的属性名 属性类型列表)) *{{ 目标对象类型 }} {return &{{ 目标对象类型 }}{每一行都是: 属性名 :属性名 (属性名小写)}
}type {{ 生成器类型 }} struct {每一行都是: 属性名 属性类型(属性名小写)
}func {{ 生成器方法名 }}() *{{ 生成器类型 }} {return &{{ 生成器类型 }}{}
}func (b *{{ 生成器类型 }}) Build() *{{ 目标对象类型 }} {return New{{ 目标对象类型 }}(逗号分隔的 b.属性名 列表
}// 对于每一个属性,遍历,做如下动作:func (b *{{ 生成器类型 }}) {{ 属性名 }}({{ 属性名(小写) }} {{ 属性类型 }}) *{{ 生成器类型 }} {b.{{ 属性名(小写) }} = {{ 属性名(小写) }}return b
}
然后,抽象出用来填充动态文本的填充对象(即要传给模版的参数对象,存储了在模板中需要用的动态信息):
type BuilderInfo struct {BuilderMethod stringBuilderClass stringBizClass stringAttrs []Attr
}type Attr struct {Name stringType 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 "Attr, string") string { return "b." + LowercaseFirst(attr.Name) })
}func MapNameAndType(attrs []Attr) []string {return util.Map[Attr, string](attrs, func(attr Attr "Attr, string") 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 的常用语法:
{{ . }} 表示顶层作用域对象,也就是你从如下方法传入的 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通过给定的业务类型来生成。
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 生成的值分别填充那几处硬写的值即可。
程序的主体,本文已经都给出来了,读者也可以将其拼接起来,做一次完型填空。
相关文章:
124. Go Template应用实例:用代码生成代码
文章目录 生成器模式生成器代码生成 本文用生成器模式作为例子,来演示如何用代码生成代码。 生成器模式 熟悉 Java 开发的同学都知道,lombok 有一个著名的注解 Builder ,只要加在类上面,就可以自动生成 Builder 模式的代码。如下…...
【AI实践】阿里云方言文本转语音TTS
最近要做一些普通话和方言demo 找一个免费工具 免费在线文字转语音工具 | edge-tts 在线体验 (bingal.com) 还有一些方言在阿里云上找了下,基于官方demo改了一下 阿里云语音合成接口说明_智能语音交互(ISI)-阿里云帮助中心 (aliyun.com) 如何下载安装、使用语音…...
java 之 各类日期格式转换
一、前言 大家在开发过程中必不可少得和日期打交道,对接别的系统时,时间日期格式不一致,每次都要转换! 从 Java1 到 Java8 将近 20 年,再加上 Java8 的普及时间、各种历史 API 兼容过渡时间。我们很多时候需要在旧时间 API 与新时…...
Nvidia黄仁勋对话Meta扎克伯格:AI和下一代计算平台的未来 | SIGGRAPH 2024对谈回顾
在今年的SIGGRAPH图形大会上,Nvidia创始人兼CEO黄仁勋与Meta创始人马克扎克伯格进行了一场长达60分钟的对谈。这场对话不仅讨论了AI的未来发展和Meta的开源哲学,还发布了不少新产品,并深入探讨了下一代计算平台的可能性。 引言 人工智能的发…...
【JAVA设计模式】适配器模式——类适配器模式详解与案例分析
前言 在软件设计中,适配器模式(Adapter Pattern)是一种结构型设计模式,旨在使不兼容的接口能够协同工作。它通过引入一个适配器类,帮助两个接口之间进行适配,使得它们能够互相操作。本文将详细介绍适配器模…...
【Vue】全局组件和局部组件
一、全局组件 定义: 全局组件是在整个Vue应用中都可以使用的组件。它们被注册在Vue的根实例上,因此可以在任何子组件的模板中被引用,而无需在每个组件中重复注册。 注册方式: 全局组件通过Vue.component方法进行注册。这个方法接…...
react引入高德地图并初始化卫星地图
react引入高德地图并初始化卫星地图 1.安装依赖 yarn add react-amap amap/amap-jsapi-loader2.初始化地图 import AMapLoader from "amap/amap-jsapi-loader"; import { FC, useEffect, useRef, useState } from "react";const HomeRight () > {con…...
2024最简七步完成 将本地项目提交到github仓库方法
2024最简七步完成 将本地项目提交到github仓库方法 文章目录 2024最简七步完成 将本地项目提交到github仓库方法一、前言二、具体步骤1、github仓库创建2、将远程仓库拉取并合并(1)初始化本地仓库(2)本地仓库与Github仓库关联&…...
前端WebSocket入门,看这篇就够啦!!
在HTML5 的早期开发过程中,由于意识到现有的 HTTP 协议在实时通信方面的不足,开发者开始探索能够在 Web 环境下实现双向实时通信的新的通信协议,提出了 WebSocket 协议的概念。 一、什么是 WebSocket? WebSocket 是一种在单个 T…...
漏洞复现-F6-11泛微-E-Cology-SQL
本文来自无问社区,更多漏洞信息可前往查看http://www.wwlib.cn/index.php/artread/artid/15575.html 0x01 产品简介 泛微协同管理应用平台e-cology是一套企业级大型协同管理平台 0x02 漏洞概述 该漏洞是由于泛微e-cology未对用户的输入进行有效的过滤࿰…...
Turbo Boost 禁用
最近在做OAI NR的时候关闭CPU 睿频的时候出了一些问题,这里我把我找到的资料记录一下: 禁用 Turbo Boost 的过程可能会因不同的 BIOS/UEFI 和操作系统设置而有所不同。以下是一些可能的原因及解决方法: 可能的原因 BIOS/UEFI 设置问题: 你的…...
假期BUUCTF小练习3
文章目录 [极客大挑战 2019]BuyFlag[BJDCTF2020]Easy MD5[HCTF 2018]admin第一种方法 直接登录第二种方法 flack session伪造第三种方法Unicode欺骗 [MRCTF2020]你传你🐎呢[护网杯 2018]easy_tornadoSSTI注入 [ZJCTF 2019]NiZhuanSiWei [极客大挑战 2019]BuyFlag 一…...
【ubuntu系统】在虚拟机内安装Ubuntu
Ubuntu系统装机 描述新装机后的常规配置, 虚拟机使用vbox terminal 打不开 CTRL ALT F3 进入命令行模式(需要返回桌面时CTRL ALT F1)root用户登入cd /etc/default vi locale LANG“en_US” 改成 LANG“en_US.UTF-8”保存修改后&…...
Python初学者必须掌握的基础知识点
Python初学者必须掌握的基础知识点包括数据类型与变量、控制结构(条件语句和循环语句)、基本数据结构(列表、元组、字典、集合)、函数与模块、以及字符串处理等。以下是对这些基础知识点及其对应代码的详细介绍: 1. …...
ESP32是什么?
ESP32是一款由乐鑫信息科技(Espressif Systems)推出的高度集成的低功耗系统级芯片(SoC),它结合了双核处理器、无线通信、低功耗特性和丰富的外设,特别适用于各种物联网(IoT)应用。以…...
jemalloc分析内存
分析内存泄漏过程中, 由于tcmalloc不能长时间开启heap profile(会不停涨内存,导致内存爆掉).尝试换jemalloc. 交叉编译: git clone https://github.com/jemalloc/jemalloc.git./autogen.sh./configure --hostaarch64-…...
【QT】qss
目录 基本语法 设置全局样式 问题 分离样式代码 方案1 方案2 选择器 概况 子控件选择器 伪类选择器 盒子模型 修改控件样式示例 按钮 属性小结 复选框 属性小结 输入框 属性小结 列表框 属性小结 渐变色 示例: 菜单栏 设置菜单栏的背景…...
Java处理大数据的技巧
大数据处理是现代计算机科学中的一个重要领域,通过高效的算法和工具,我们可以从大量数据中提取有价值的信息。本文将介绍一些处理大数据的技巧和策略,并讨论如何通过Java与MySQL实现高效的大数据处理。 一、什么是大数据处理? 大…...
JavaScript基础——JavaScript常见语句(判断语句、循环语句、控制流语句)
JavaScript提供了丰富的语句来控制程序的执行流程,包括用于条件判断的if、switch和三元运算符,以及用于循环的for、while、do...while、for...in和for...of。此外,还有控制流语句如break、continue和return。 判断语句 if 语句 if 语句&…...
材质球向shader传值失败
unity中导入spine模型,当模型挂载SkeletonMecanim组件后,发现材质球向shader传值失败,改为SetPropertyBlock后可行。 //spine模型使用材质球传参数,当spine模型上挂载有SkeletonMecanim的情况下,会传值失败!!!!// for…...
超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...
【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...
UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...
智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...
三分算法与DeepSeek辅助证明是单峰函数
前置 单峰函数有唯一的最大值,最大值左侧的数值严格单调递增,最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值,最小值左侧的数值严格单调递减,最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...
(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
