前言
大家好,这次是我参加 Cloudwego Study Group 第二期的第三篇文章。
Hertz 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势,并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。
这篇文章和往期的不一样,在之前的第一篇和第二篇的文章中我介绍了我自己对于 hz 代码生成工具的理解并对其中执行逻辑顺序进行了简单的描述。这次,我从我自己的需求出发,从自身角度对 hz 进行些许简易的改造,以满足个人的定制化需求。
我要在哪里进行改动?
这次的改动从自定义模板开始,首先我们可以来看看官方文档中的自定义模板的最简单的示例:
layouts:
# 生成的 handler 的目录,只有目录下有文件才会生成
- path: biz/handler/
delims:
- ""
- ""
body: ""
# 生成的 model 的目录,只有目录下有文件才会生成
- path: biz/model/
delims:
- ""
- ""
body: ""
# 项目 main 文件,
- path: main.go
delims:
- ""
- ""
body: "// Code generated by hertz generator.\n\npackage main\n\nimport (\n\t"github.com/cloudwego/hertz/pkg/app/server"\n)\n\nfunc
main() {\n\th := server.Default()\n\n\tregister(h)\n\th.Spin()\n}\n"
# go.mod 文件,需要模板渲染数据{{.GoModule}}才能生成
- path: go.mod
delims:
- '{{'
- '}}'
body: "module {{.GoModule}}\n{{- if .UseApacheThrift}}\nreplace github.com/apache/thrift
=> github.com/apache/thrift v0.13.0\n{{- end}}\n"
# .gitignore 文件
- path: .gitignore
delims:
- ""
- ""
body: "*.o\n*.a\n*.so\n_obj\n_test\n*.[568vq]\n[568vq].out\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n_testmain.go\n*.exe\n*.exe~\n*.test\n*.prof\n*.rar\n*.zip\n*.gz\n*.psd\n*.bmd\n*.cfg\n*.pptx\n*.log\n*nohup.out\n*settings.pyc\n*.sublime-project\n*.sublime-workspace\n!.gitkeep\n.DS_Store\n/.idea\n/.vscode\n/output\n*.local.yml\ndumped_hertz_remote_config.json\n\t\t
\ "
# .hz 文件,包含 hz 版本,是 hz 创建的项目的标志,不需要传渲染数据
- path: .hz
delims:
- '{{'
- '}}'
body: "
// Code generated by hz. DO NOT EDIT.
hz version: v{{.hzVersion}}"
# ping 自带 ping 的 handler
- path: biz/handler/ping.go
delims:
- ""
- ""
body: "// Code generated by hertz generator.\n\npackage handler\n\nimport (\n\t"context"\n\n\t"github.com/cloudwego/hertz/pkg/app"\n\t"github.com/cloudwego/hertz/pkg/common/utils"\n)\n\n//
Ping .\nfunc Ping(ctx context.Context, c *app.RequestContext) {\n\tc.JSON(200,
utils.H{\n\t\t"message": "pong",\n\t})\n}\n"
# 定义路由注册的文件,需要模板渲染数据{{.RouterPkgPath}}才能生成
- path: router_gen.go
delims:
- ""
- ""
body: "// Code generated by hertz generator. DO NOT EDIT.\n\npackage main\n\nimport (\n\t"github.com/cloudwego/hertz/pkg/app/server"\n\trouter
"{{.RouterPkgPath}}"\n)\n\n// register registers all routers.\nfunc register(r
*server.Hertz) {\n\n\trouter.GeneratedRegister(r)\n\n\tcustomizedRegister(r)\n\n}\n"
# 自定义路由注册的文件
- path: router.go
delims:
- ""
- ""
body: "// Code generated by hertz generator.\n\npackage main\n\nimport (\n\t"github.com/cloudwego/hertz/pkg/app/server"\n\thandler
"{{.GoModule}}/biz/handler"\n)\n\n// customizeRegister registers customize routers.\nfunc
customizedRegister(r *server.Hertz){\n\tr.GET("/ping", handler.Ping)\n\n\t//
your code ...\n}\n"
# 默认路由注册文件,不要修改
- path: biz/router/register.go
delims:
- ""
- ""
body: "// Code generated by hertz generator. DO NOT EDIT.\n\npackage router\n\nimport (\n\t"github.com/cloudwego/hertz/pkg/app/server"\n)
\n\n// GeneratedRegister registers routers generated by IDL.\n
func GeneratedRegister(r *server.Hertz) {\n\t//INSERT_POINT: DO NOT DELETE THIS LINE!\n}\n
很明显,这里的 body 的代码可读性并不是那么的高,因为它是进行过序列化的代码内容。我们阅读困难的同时,程序阅读起来十分容易, 因为里面充满了类似 \t ,\n 的转义符号。
对我而言,我对这个方式有些许不满:
- 它的可读性写出来太差
- body 中的内容肯定是在 yaml 文件外部编写,并进行一定处理再粘贴进 yaml 内的。不然手写转义符想想都头疼。
那有没有一种可能我通过修改 hz 代码可以解决以上的问题?我觉得可以试试看。
我从哪里开始对代码改动?
hz 到底从上面时候开始读取 body 的代码的?
hertz/cmd/hz/internal/generator/template.go 106 行和 hertz/cmd/hz/internal/generator/package.go 和 95 行
都有以下相似的代码逻辑,以下为伪代码:
if tpl, err = tpl.Parse(layout.Body); err != nil {
return fmt.Errorf("parse template '%s' failed, err: %v", path, err.Error())
}
它通过读取 layout.Body 的转义符字符串达到了解析 body 的作用。
这样的话我可以做一个桥梁,在外部编写不带转义符的模板文件,再通过在 yaml 配置文件中添加模板文件的路径。
在 parse 之前,对模板文件进行读取和序列化,不就可以将文件弄得清晰一些了吗?
预想的方案
在上面我说了通过添加自定义模板的路径来替换 body, 那下面是我预想的伪代码和目录结构:
layouts:
# path只表示handler.go的模板,具体的handler路径由默认路径和handler_dir决定
- path: handler.go
templatePath: template/package/handler.go.tmpl
# path只表示router.go的模板,其路径固定在:biz/router/namespace/
- path: router.go
templatePath: template/package/router.go.tmpl
# path只表示register.go的模板,register的路径固定为biz/router/register.go
- path: register.go
templatePath: template/package/register.go.tmpl
- path: model.go
templatePath: template/package/model.go.tmpl
# path只表示middleware.go的模板,middleware的路径和router.go一样为:biz/router/namespace/
- path: middleware.go
templatePath: template/package/middleware.go.tmpl
# path只表示client.go的模板,client代码的生成路径由用户指定"${client_dir}"
- path: client.go
templatePath: template/package/client.go.tmpl
package_templatePath
├── idl
│ └── hello.thrift
└── template
├── package // 存放模板的位置,和上文对应
│ ├── client.go.tmpl
│ ├── handler.go.tmpl
│ ├── middleware.go.tmpl
│ ├── model.go.tmpl
│ ├── register.go.tmpl
│ └── router.go.tmpl
└── package.yaml
然后再通过编写不加转义符模板文件不就实现我的构想了吗?说干就干!
实际的改动
hertz/cmd/hz/internal/generator/template.go line 40
在原本的 Template struct 中添加 TemplatePath 选项,用于接收模板文件相对位置
type Template struct {
Path string `yaml:"path"` // The generated path and its filename, such as biz/handler/ping.go
Delims [2]string `yaml:"delims"` // Template Action Instruction Identifier,default: "{{}}"
Body string `yaml:"body"` // Render template, currently only supports go template syntax
// new feature
TemplatePath string `yaml:"templatePath"` // The template body path
}
hertz/cmd/hz/internal/generator/template.go line.108
Line 1: 使用 Body 字段,复用原本逻辑
Line 8: 使用 TemplatePath 字段,读取字段路径的文件内容,进行 Parse()
Line 21: 保证 Body 和 TemplatePath 字段只可能有一个使用,防止出现混乱
// use Body
if l.Body != "" && l.TemplatePath == "" {
if tpl, err = tpl.Parse(l.Body); err != nil {
return fmt.Errorf("parse template '%s' failed, err: %v", path, err.Error())
}
}
// use TemplatePath
if l.TemplatePath != "" && l.Body == "" {
bs, err := filepath.Abs(l.TemplatePath)
if err != nil {
return fmt.Errorf("get absolute path of template '%s' failed, err: %v", l.TemplatePath, err.Error())
}
str, err := ioutil.ReadFile(abs)
if err != nil {
fmt.Println("read template file fail", err.Error())
}
if tpl, err = tpl.Parse(string(str)); err != nil {
return fmt.Errorf("parse template '%s' failed, err: %v", path, err.Error())
}
// use Body and TemplatePath
} else if l.TemplatePath != "" && l.Body != "" {
return fmt.Errorf("only one of Body and TemplatePath can be used at the same time")
}
上文中的 package.go 的逻辑也做同等处理,这里就不在赘述。这里是代码的全部改动了。
hertz-examples/hz/template/package_templatePath/template/package/router.go.tmpl
如何使用?
github.com/Skyenought/… 这是我分支的示例代码,必须通过搭配以上改动才能进行使用。
github.com/Skyenought/… 这是经过我改动的 hz 工具的 hertz 版本,需要通过 go build 自行编译才能进行使用。
我更推荐大家自己对照我的实现进行改动,只有自己敲过的代码才是真正属于自己的!
本文正在参加技术专题18期-聊聊Go语言框架
\