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…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...
unix/linux,sudo,其发展历程详细时间线、由来、历史背景
sudo 的诞生和演化,本身就是一部 Unix/Linux 系统管理哲学变迁的微缩史。来,让我们拨开时间的迷雾,一同探寻 sudo 那波澜壮阔(也颇为实用主义)的发展历程。 历史背景:su的时代与困境 ( 20 世纪 70 年代 - 80 年代初) 在 sudo 出现之前,Unix 系统管理员和需要特权操作的…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
【SpringBoot自动化部署】
SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一,能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时,需要添加Git仓库地址和凭证,设置构建触发器(如GitHub…...