Hertz 源码解析 —— 对 hz 工具的小改动

427 阅读6分钟

前言

大家好,这次是我参加 Cloudwego Study Group 第二期的第三篇文章。

Hertz 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttpginecho 的优势,并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。如今越来越多的微服务选择使用 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 的转义符号。

对我而言,我对这个方式有些许不满:

  1. 它的可读性写出来太差
  2. 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语言框架

\