hz 自定义模板使用

Hertz 提供的命令行工具(以下称为"hz")支持自定义模板功能,包括:

  • 自定义 layout 模板(即生成代码的目录结构)
  • 自定义 package 模板(即与 service 相关的代码结构,包括 handler、router 等)

用户可自己提供模板以及渲染参数,并结合 hz 的能力,来完成自定义的代码生成结构。

自定义 layout 模板

用户可根据默认模板来修改或重写,从而满足自身需求

hz 利用了 go template 支持以 “yaml” 的格式定义模板,并使用 “json” 定义模板渲染数据。

所谓的 layout 模板是指整个项目的结构,这些结构与具体的 idl 定义无关,不需要 idl 也可以直接生成,默认的结构如下:

.
├── biz
│   ├── handler
│   │   └── ping.go
│   │   └── ****.go               // 按照服务划分的 handler 集合,位置可根据 handler_dir 改变
│   ├── model
│   │   └── model.go              // idl 生成的 struct,位置可根据 model_dir 改变
│   └── router // 未开发自定义 dir
│        └── register.go          // 路由注册,用来调用具体的路由注册
│             └── route.go        // 具体路由注册位置
│             └── middleware.go   // 默认 middleware 生成位置
├── .hz                           // hz 创建代码标志
├── go.mod
├── main.go                       // 启动入口
├── router.go                     // 用户自定义路由写入位置
└── router_gen.go                 // hz 生成的路由注册调用

IDL

// hello.thrift
namespace go hello.example

struct HelloReq {
    1: string Name (api.query="name");
}

struct HelloResp {
    1: string RespBody;
}


service HelloService {
    HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");
}

命令

hz new --mod=github.com/hertz/hello --idl=./hertzDemo/hello.thrift --customize_layout=template/layout.yaml:template/data.json

默认 layout 模板的含义

注:以下的 body 均为 go template

layouts:
  # 生成的 handler 的目录,只有目录下有文件才会生成
  - path: biz/handler/
    delims:
      - ""
      - ""
    body: ""
  # 生成的 model 的目录,只有目录下有文件才会生成
  - path: biz/model/
    delims:
      - ""
      - ""
    body: ""
  # 项目 main 文件,
  - path: main.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator.

      package main

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
      )

      func main() {
      	h := server.Default()

      	register(h)
      	h.Spin()
      }
  # go.mod 文件,需要模板渲染数据{{.GoModule}}才能生成
  - path: go.mod
    delims:
      - '{{'
      - '}}'
    body: |-
      module {{.GoModule}}
      {{- if .UseApacheThrift}}
      replace github.com/apache/thrift => github.com/apache/thrift v0.13.0
      {{- end}}
  # .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: {{.hzVersion}}
  # ping 自带 ping 的 handler
  - path: biz/handler/ping.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator.

      package handler

      import (
      	"context"

      	"github.com/cloudwego/hertz/pkg/app"
      	"github.com/cloudwego/hertz/pkg/common/utils"
      )

      // Ping .
      func Ping(ctx context.Context, c *app.RequestContext) {
      	c.JSON(200, utils.H{
      		"message": "pong",
      	})
      }
  # 定义路由注册的文件,需要模板渲染数据{{.RouterPkgPath}}才能生成
  - path: router_gen.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package main

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
      	router "{{.RouterPkgPath}}"
      )

      // register registers all routers.
      func register(r *server.Hertz) {

      	router.GeneratedRegister(r)

      	customizedRegister(r)
      }
  # 自定义路由注册的文件
  - path: router.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator.

      package main

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
      	handler "{{.HandlerPkgPath}}"
      )

      // customizeRegister registers customize routers.
      func customizedRegister(r *server.Hertz){
      	r.GET("/ping", handler.Ping)

      	// your code ...
      }
  # 默认路由注册文件,不要修改
  - path: biz/router/register.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package router

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
      )

      // GeneratedRegister registers routers generated by IDL.
      func GeneratedRegister(r *server.Hertz){
      	//INSERT_POINT: DO NOT DELETE THIS LINE!
      }

模板渲染参数文件的含义

当指定了自定义模板以及渲染数据后,此时命令行指定的选项将不会被作为渲染数据,因此,模板中的渲染数据需要用户自己定义。

hz 使用了"json"来指定渲染数据,下面进行介绍

{
  // 全局的渲染参数
  "*": {
    "GoModule": "github.com/hz/test", // 要和命令行指定的一致,否则后续生成model、handler等代码将使用命令行指定的mod,导致出现不一致。
    "ServiceName": "p.s.m", // 要和命令行指定的一致
    "UseApacheThrift": false // 根据是否使用"thrift"设置"true"/"false"
  },
  // router_gen.go 路由注册的渲染数据,
  // "biz/router"指向默认idl注册的路由代码的module,不要修改
  "router_gen.go": {
    "RouterPkgPath": "github.com/hz/test/biz/router"
  }
}

自定义一个 layout 模板

目前,hz 生成的项目 layout 已经是一个 hertz 项目最最最基础的骨架了,所以不建议删除现有的模板里的文件。

不过如果用户想要一个别的 layout ,当然也可以根据自身需求来删除相应的文件(除"biz/register.go"外,其余都可以动)

我们十分欢迎用户来贡献自己的模板

下面假设用户只想要 “main.go” 以及 “go.mod” 文件,那么我们对默认模板进行修改,如下:

template:

// layout.yaml
layouts:
  # 项目 main 文件,
  - path: main.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator.

      package main

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
        "{{.GoModule}}/biz/router"
      )

      func main() {
      	h := server.Default()

        router.GeneratedRegister(h)
        // do what you wanted
        // add some render data: {{.MainData}}

      	h.Spin()
      }

  # go.mod 文件,需要模板渲染数据{{.GoModule}}才能生成
  - path: go.mod
    delims:
      - '{{'
      - '}}'
    body: |-
      module {{.GoModule}}
      {{- if .UseApacheThrift}}
      replace github.com/apache/thrift => github.com/apache/thrift v0.13.0
      {{- end}}
  # 默认路由注册文件,没必要修改
  - path: biz/router/register.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package router

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
      )

      // GeneratedRegister registers routers generated by IDL.
      func GeneratedRegister(r *server.Hertz){
      	//INSERT_POINT: DO NOT DELETE THIS LINE!
      }

render data:

{
  "*": {
    "GoModule": "github.com/hertz/hello",
    "ServiceName": "hello",
    "UseApacheThrift": true
  },
  "main.go": {
    "MainData": "this is customized render data"
  }
}

命令:

hz new --mod=github.com/hertz/hello --idl=./hertzDemo/hello.thrift --customize_layout=template/layout.yaml:template/data.json

自定义 package 模板

hz 模板的模板地址:

用户可根据默认模板来修改或重写,从而符合自身需求

  • 所谓的 package 模板是指与 idl 定义相关的服务代码,这部分代码涉及到定义 idl 时指定的service、go_package/namespace等,主要包括以下几部分:
  • handler.go : 处理函数逻辑
  • router.go:具体的 idl 定义的服务的路由注册逻辑
  • register.go:调用router.go中内容的逻辑
  • model代码:生成的 go struct;不过由于目前使用插件来生成model代码工具没权限来修改model的模板,所以这部分功能先不开放

命令

# 之后会提供 package 模板渲染数据,所以输入命令的时候先保留了"k-v"的形式,customize_package 后需要加":"
hz new --mod=github.com/hertz/hello --handler_dir=handler_test --idl=hertzDemo/hello.thrift --customize_package=template/package.yaml:

默认 package 模板

注意:自定义 package 模板没有提供渲染数据的功能,这里主要是因为这些渲染数据是 hz 工具解析生成的,所以暂时不提供自己写渲染数据的功能。可以修改下模板里面与渲染数据无关的部分,以满足自身需求。

# 以下数据都是 yaml marshal 得到的,所以可能看起来比较乱
layouts:
  # path只表示handler.go的模板,具体的handler路径由默认路径和handler_dir决定
  - path: handler.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // Code generated by hertz generator.

      package {{.PackageName}}

      import (
      	"context"

      	"github.com/cloudwego/hertz/pkg/app"

      {{- range $k, $v := .Imports}}
      	{{$k}} "{{$v.Package}}"
      {{- end}}
      )

      {{range $_, $MethodInfo := .Methods}}
      {{$MethodInfo.Comment}}
      func {{$MethodInfo.Name}}(ctx context.Context, c *app.RequestContext) {
      	var err error
      	{{if ne $MethodInfo.RequestTypeName "" -}}
      	var req {{$MethodInfo.RequestTypeName}}
      	err = c.BindAndValidate(&req)
      	if err != nil {
      		c.String(400, err.Error())
      		return
      	}
      	{{end}}
      	resp := new({{$MethodInfo.ReturnTypeName}})

      	c.{{.Serializer}}(200, resp)
      }
      {{end}}
  # path只表示router.go的模板,其路径固定在:biz/router/namespace/
  - path: router.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package {{$.PackageName}}

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"

      	{{range $k, $v := .HandlerPackages}}{{$k}} "{{$v}}"{{end}}
      )

      /*
       This file will register all the routes of the services in the master idl.
       And it will update automatically when you use the "update" command for the idl.
       So don't modify the contents of the file, or your code will be deleted when it is updated.
       */

      {{define "g"}}
      {{- if eq .Path "/"}}r
      {{- else}}{{.GroupName}}{{end}}
      {{- end}}

      {{define "G"}}
      {{- if ne .Handler ""}}
      	{{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.MiddleWare}}Mw(), {{.Handler}})...)
      {{- end}}
      {{- if ne (len .Children) 0}}
      {{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.MiddleWare}}Mw()...)
      {{- end}}
      {{- range $_, $router := .Children}}
      {{- if ne .Handler ""}}
      	{{template "G" $router}}
      {{- else}}
      	{	{{template "G" $router}}
      	}
      {{- end}}
      {{- end}}
      {{- end}}

      // Register register routes based on the IDL 'api.${HTTP Method}' annotation.
      func Register(r *server.Hertz) {
      {{template "G" .Router}}
      }
  # path只表示register.go的模板,register的路径固定为biz/router/register.go
  - path: register.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package router

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
      	{{$.PkgAlias}} "{{$.Pkg}}"
      )

      // GeneratedRegister registers routers generated by IDL.
      func GeneratedRegister(r *server.Hertz){
      	//INSERT_POINT: DO NOT DELETE THIS LINE!
      	{{$.PkgAlias}}.Register(r)
      }
  - path: model.go
    delims:
      - ""
      - ""
    body: ""
  # path只表示middleware.go的模板,middleware的路径和router.go一样为:biz/router/namespace/
  - path: middleware.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // Code generated by hertz generator.

      package {{$.PackageName}}

      import (
      	"github.com/cloudwego/hertz/pkg/app"
      )

      {{define "M"}}
      func {{.MiddleWare}}Mw() []app.HandlerFunc {
      	// your code...
      	return nil
      }
      {{range $_, $router := $.Children}}{{template "M" $router}}{{end}}
      {{- end}}

      {{template "M" .Router}}
  # path只表示client.go的模板,client代码的生成路径由用户指定"${client_dir}"
  - path: client.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // Code generated by hertz generator.

      package {{$.PackageName}}

      import (
          "github.com/cloudwego/hertz/pkg/app/client"
      	"github.com/cloudwego/hertz/pkg/common/config"
      )

      type {{.ServiceName}}Client struct {
      	client * client.Client
      }

      func New{{.ServiceName}}Client(opt ...config.ClientOption) (*{{.ServiceName}}Client, error) {
      	c, err := client.NewClient(opt...)
      	if err != nil {
      		return nil, err
      	}

      	return &{{.ServiceName}}Client{
      		client: c,
      	}, nil
      }
  # handler_single表示单独的handler模板,用于update的时候更新每一个新增的handler
  - path: handler_single.go
    delims:
      - '{{'
      - '}}'
    body: |+
      {{.Comment}}
      func {{.Name}}(ctx context.Context, c *app.RequestContext) {
      // this my demo
      	var err error
      	{{if ne .RequestTypeName "" -}}
      	var req {{.RequestTypeName}}
      	err = c.BindAndValidate(&req)
      	if err != nil {
      		c.String(400, err.Error())
      		return
      	}
      	{{end}}
      	resp := new({{.ReturnTypeName}})

      	c.{{.Serializer}}(200, resp)
      }
  # middleware_single表示单独的middleware模板,用于update的时候更新每一个新增的middleware_single
  - path: middleware_single.go
    delims:
      - '{{'
      - '}}'
    body: |+
      func {{.MiddleWare}}Mw() []app.HandlerFunc {
      	// your code...
      	return nil
      }

自定义一个 package 模板

与 layout 模板一样,用户同样可以自定义 package 模板。

就 package 提供的模板来说,一般用户可能只有自定义 handler.go 的模板的需求,因为router.go/middleware.go/register.go 一般与 idl 定义相关而用户无需关心,因此 hz 目前也将这些模板生成的位置固定了,一般也无需修改。

因此,用户可根据自身的需求来自定义生成的 handler 模板,加速开发速度;但是由于默认的 handler 模板集成了一些 model 的信息以及 package 信息,所以需要 hz 工具来提供渲染数据。这部分用户可根据自身情况酌情来修改,一般建议留下 model 信息。

下面给出一个简单的自定义 handler 模板的示例,在 handler 里加入一些注释:

template:

# 以下数据都是 yaml marshal 得到的,所以可能看起来比较乱
layouts:
  # path只表示handler.go的模板,具体的handler路径由默认路径和handler_dir决定
  - path: handler.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // this is my custom handler.

      package {{.PackageName}}

      import (
      	"context"

      	"github.com/cloudwego/hertz/pkg/app"

      {{- range $k, $v := .Imports}}
      	{{$k}} "{{$v.Package}}"
      {{- end}}
      )

      {{range $_, $MethodInfo := .Methods}}
      {{$MethodInfo.Comment}}
      func {{$MethodInfo.Name}}(ctx context.Context, c *app.RequestContext) {
        //  you can code something
      	var err error
      	{{if ne $MethodInfo.RequestTypeName "" -}}
      	var req {{$MethodInfo.RequestTypeName}}
      	err = c.BindAndValidate(&req)
      	if err != nil {
      		c.String(400, err.Error())
      		return
      	}
      	{{end}}
      	resp := new({{$MethodInfo.ReturnTypeName}})

      	c.{{.Serializer}}(200, resp)
      }
      {{end}}
  # path只表示router.go的模板,其路径固定在:biz/router/namespace/
  - path: router.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package {{$.PackageName}}

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"

      	{{range $k, $v := .HandlerPackages}}{{$k}} "{{$v}}"{{end}}
      )

      /*
       This file will register all the routes of the services in the master idl.
       And it will update automatically when you use the "update" command for the idl.
       So don't modify the contents of the file, or your code will be deleted when it is updated.
       */

      {{define "g"}}
      {{- if eq .Path "/"}}r
      {{- else}}{{.GroupName}}{{end}}
      {{- end}}

      {{define "G"}}
      {{- if ne .Handler ""}}
      	{{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.MiddleWare}}Mw(), {{.Handler}})...)
      {{- end}}
      {{- if ne (len .Children) 0}}
      {{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.MiddleWare}}Mw()...)
      {{- end}}
      {{- range $_, $router := .Children}}
      {{- if ne .Handler ""}}
      	{{template "G" $router}}
      {{- else}}
      	{	{{template "G" $router}}
      	}
      {{- end}}
      {{- end}}
      {{- end}}

      // Register register routes based on the IDL 'api.${HTTP Method}' annotation.
      func Register(r *server.Hertz) {
      {{template "G" .Router}}
      }
  # path只表示register.go的模板,register的路径固定为biz/router/register.go
  - path: register.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package router

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
      	{{$.PkgAlias}} "{{$.Pkg}}"
      )

      // GeneratedRegister registers routers generated by IDL.
      func GeneratedRegister(r *server.Hertz){
      	//INSERT_POINT: DO NOT DELETE THIS LINE!
      	{{$.PkgAlias}}.Register(r)
      }
  - path: model.go
    delims:
      - ""
      - ""
    body: ""
  # path只表示middleware.go的模板,middleware的路径和router.go一样为:biz/router/namespace/
  - path: middleware.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // Code generated by hertz generator.

      package {{$.PackageName}}

      import (
      	"github.com/cloudwego/hertz/pkg/app"
      )

      {{define "M"}}
      func {{.MiddleWare}}Mw() []app.HandlerFunc {
      	// your code...
      	return nil
      }
      {{range $_, $router := $.Children}}{{template "M" $router}}{{end}}
      {{- end}}

      {{template "M" .Router}}
  # path只表示client.go的模板,client代码的生成路径由用户指定"${client_dir}"
  - path: client.go
    delims:
      - '{{'
      - '}}'
    body: |-
      // Code generated by hertz generator.

      package {{$.PackageName}}

      import (
          "github.com/cloudwego/hertz/pkg/app/client"
      	"github.com/cloudwego/hertz/pkg/common/config"
      )

      type {{.ServiceName}}Client struct {
      	client * client.Client
      }

      func New{{.ServiceName}}Client(opt ...config.ClientOption) (*{{.ServiceName}}Client, error) {
      	c, err := client.NewClient(opt...)
      	if err != nil {
      		return nil, err
      	}

      	return &{{.ServiceName}}Client{
      		client: c,
      	}, nil
      }
  # handler_single表示单独的handler模板,用于update的时候更新每一个新增的handler
  - path: handler_single.go
    delims:
      - '{{'
      - '}}'
    body: |+
      {{.Comment}}
      func {{.Name}}(ctx context.Context, c *app.RequestContext) {
      // this my demo
      	var err error
      	{{if ne .RequestTypeName "" -}}
      	var req {{.RequestTypeName}}
      	err = c.BindAndValidate(&req)
      	if err != nil {
      		c.String(400, err.Error())
      		return
      	}
      	{{end}}
      	resp := new({{.ReturnTypeName}})

      	c.{{.Serializer}}(200, resp)
      }
  # middleware_single表示单独的middleware模板,用于update的时候更新每一个新增的middleware_single
  - path: middleware_single.go
    delims:
      - '{{'
      - '}}'
    body: |+
      func {{.MiddleWare}}Mw() []app.HandlerFunc {
      	// your code...
      	return nil
      }

命令:

# 之后会提供 package 模板渲染数据,所以输入命令的时候先保留了"k-v"的形式,customize_package 后需要加":"
hz new --mod=github.com/hertz/hello --handler_dir=handler_test --idl=hertzDemo/hello.thrift --customize_package=template/package.yaml:

生成handler如下:

// this is my custom handler.

package example

import (
	"context"

	"github.com/cloudwego/hertz/pkg/app"
	example "test/test2/biz/model/hello/example"
)

// HelloMethod .
// @router /hello [GET]
func HelloMethod(ctx context.Context, c *app.RequestContext) {
	//  you can code something
	var err error
	var req example.HelloReq
	err = c.BindAndValidate(&req)
	if err != nil {
		c.String(400, err.Error())
		return
	}

	resp := new(example.HelloResp)

	c.JSON(200, resp)
}

// OtherMethod .
// @router /other [POST]
func OtherMethod(ctx context.Context, c *app.RequestContext) {
	//  you can code something
	var err error
	var req example.OtherReq
	err = c.BindAndValidate(&req)
	if err != nil {
		c.String(400, err.Error())
		return
	}

	resp := new(example.OtherResp)

	c.JSON(200, resp)
}

注意事项

使用 layout 模板的注意事项

当用户使用了 layout 自定义模板后,那么生成的 layout 和渲染数据都由用户接管,所以用户需要提供其定义的 layout 的渲染数据。

使用 package 模板的注意事项

一般来说,用户使用 package 模板的时候大多数是为了修改默认的 handler 模板;不过,目前 hz 没有提供单个 handler 的模板,所以当 update 已经存在的 handler 文件时,会使用默认 handler 模板在 handler 文件尾追加新的 handler function。当对应的 handler 文件不存在的时候,才会使用自定义模板来生成 handler 文件。