在面对复杂且千差万别的业务场景时,通过自定义模板,我们可以生成高效、可维护的代码。在本节中,我们将介绍如何在 goctl
中使用 text/template
,并分享一些最佳实践。
近期,我们也将给出 goctl 模板参数规范,并在新版本提供支持,以便大家更好的根据业务需求进行定制。
1. 介绍
text/template
是 Go
语言标准库中的一个包,用于处理文本模板。通过使用模板,可以将逻辑与表现分离,使代码更加清晰和易于维护。goctl
是一个用于生成 Go
代码的工具,通过结合 text/template
,我们可以实现高效的代码生成。
2. text/template 基础
text/template
包提供了丰富的功能来处理文本模板。以下是一些关键概念和用法:
2.1 定义模板
在定义模板时,我们使用双花括号 {{ }}
来表示模板动作。以下是一个简单的模板示例:
const tmpl = `Hello, {{.Name}}!`
2.2 解析和执行模板
我们可以使用 template.New
和 template.Parse
方法来解析模板,然后使用 Execute 方法来执行模板。
package main import ( "os" "text/template" ) func main() { const tmpl = `Hello, {{.Name}}!` t, err := template.New("example").Parse(tmpl) if err != nil { panic(err) } data := map[string]string{ "Name": "World", } err = t.Execute(os.Stdout, data) if err != nil { panic(err) } }
2.3 模板函数
text/template
允许我们自定义模板函数,以扩展模板的功能。我们可以使用 template.FuncMap
来注册自定义函数。
func ToUpper(s string) string { return strings.ToUpper(s) } t.Funcs(template.FuncMap{ "ToUpper": ToUpper, })
更多基础功能请参考往期文章。
3. goctl 模板介绍
goctl 目前支持 api
、docker
、gateway
、kube
、model
、mongo
、newapi
、rpc
指令的模板定制化。对应的模板文件树如下:
. ├── api │ ├── config.tpl │ ├── context.tpl │ ├── etc.tpl │ ├── handler.tpl │ ├── logic.tpl │ ├── main.tpl │ ├── middleware.tpl │ ├── route-addition.tpl │ ├── routes.tpl │ ├── template.tpl │ └── types.tpl ├── docker │ └── docker.tpl ├── gateway │ ├── etc.tpl │ └── main.tpl ├── kube │ ├── deployment.tpl │ └── job.tpl ├── model │ ├── customized.tpl │ ├── delete.tpl │ ├── err.tpl │ ├── field.tpl │ ├── find-one-by-field-extra-method.tpl │ ├── find-one-by-field.tpl │ ├── find-one.tpl │ ├── import-no-cache.tpl │ ├── import.tpl │ ├── insert.tpl │ ├── interface-delete.tpl │ ├── interface-find-one-by-field.tpl │ ├── interface-find-one.tpl │ ├── interface-insert.tpl │ ├── interface-update.tpl │ ├── model-gen.tpl │ ├── model-new.tpl │ ├── model.tpl │ ├── table-name.tpl │ ├── tag.tpl │ ├── types.tpl │ ├── update.tpl │ └── var.tpl ├── mongo │ ├── err.tpl │ ├── model.tpl │ ├── model_custom.tpl │ └── model_types.tpl ├── newapi │ └── newtemplate.tpl └── rpc ├── call.tpl ├── config.tpl ├── etc.tpl ├── logic-func.tpl ├── logic.tpl ├── main.tpl ├── server-func.tpl ├── server.tpl ├── svc.tpl └── template.tpl 9 directories, 54 files
3.1 goctl 模板变量
在进行定制化之前,我们需要知道 goctl
内置提供了哪些模板变量,以及每个变量都是什么作用,否则对于定制化可能会有一些受阻。关于模板变量请参考官网介绍(https://go-zero.dev/docs/tutorials/customization/template)。以下是抽样 api logic
文件的模板及变量内容。
logic.tpl
package {{.pkgName}} import ( {{.imports}} ) type {{.logic}} struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } {{if .hasDoc}}{{.doc}}{{end}} func New{{.logic}}(ctx context.Context, svcCtx *svc.ServiceContext) *{{.logic}} { return &{{.logic}}{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *{{.logic}}) {{.function}}({{.request}}) {{.responseType}} { // todo: add your logic here and delete this line {{.returnString}} }
模板注入对象为 map[string]any
map[string]any{ "pkgName": subDir[strings.LastIndex(subDir, "/")+1:], "imports": imports, "logic": strings.Title(logic), "function": strings.Title(strings.TrimSuffix(logic, "Logic")), "responseType": responseString, "returnString": returnString, "request": requestString, "hasDoc": len(route.JoinedDoc()) > 0, "doc": getDoc(route.JoinedDoc()), }
pipeline变量 | 类型 | 说明 |
.pkgName | string | 包名 |
.imports | string | 导入包 |
.logic | string | 逻辑结构体名称 |
.hasDoc | bool | 是否有文档注释 |
.doc | string | 文档注释 |
.function | string | logic 函数名称 |
.request | string | 请求体表达式,包含参数名称,参数类型 |
.responseType | string | 响应类型体表达式,包含参数名称,参数类型 |
.returnString | string | 返回语句,返回的结构体 |
3.2 模板定制化
这里描述的模板定制化均指不修改 goctl
源码的前提下操作模板文件的过程。
首先,模板定制化并非万能的,模板定制化可以:
- 引入外部
pkg
,比如引入http
的参数校验包 - 增加公用业务逻辑,如生成
code-msg
的数据格式封装 - 替换
goctl
内部一些生成逻辑,如定制化请求参数校验 - 丰富业务实现,如对
model
增加分页查询
模板定制化不能做什么?
- 新增模板文件,新增模板文件
goctl
不会识别 - 自定义模板变量,对于非
goctl
注入的模板变量,模板是不会识别的,因此渲染出来的代码肯定是不符合预期的。 - 不能从环境变量读取
3.3 模板定制化最佳实践
实践1:http 生成 code-msg 响应格式
统一的 code-msg
格式可以有利于前端数据格式解析,go-zero
并未提供统一的封装,因此需要我们按照自己的业务需求进行改造,此过程可以通过修改模板来实现,加入我们想要响应的数据格式参考如下:
{ "code": 0, "msg: "ok", "data":{ ... } }
思路:响应体的控制是在 handler
中返回的,因此我们只要找到 api
下的 handler.tpl
就可以进行更改了,原模板内容:
package {{.PkgName}} import ( "net/http" "github.com/zeromicro/go-zero/rest/httpx" {{.ImportPackages}} ) {{if .HasDoc}}{{.Doc}}{{end}} func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { {{if .HasRequest}}var req types.{{.RequestType}} if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return } {{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx) {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}}) if err != nil { httpx.ErrorCtx(r.Context(), w, err) } else { {{if .HasResp}}httpx.OkJsonCtx(r.Context(), w, resp){{else}}httpx.Ok(w){{end}} } } }
由于没有现成的包提供 code-msg
的解决方案,因此在 zeromicro
提供了一个 x
的扩展包可以来解决此问题。详情可参考 https://github.com/zeromicro/x,我们借助 x
模块下面的 http
包来定制化模板,以下是其部分源码:
// BaseResponse is the base response struct. type BaseResponse[T any] struct { // Code represents the business code, not the http status code. Code int `json:"code" xml:"code"` // Msg represents the business message, if Code = BusinessCodeOK, // and Msg is empty, then the Msg will be set to BusinessMsgOk. Msg string `json:"msg" xml:"msg"` // Data represents the business data. Data T `json:"data,omitempty" xml:"data,omitempty"` } type baseXmlResponse[T any] struct { XMLName xml.Name `xml:"xml"` Version string `xml:"version,attr"` Encoding string `xml:"encoding,attr"` BaseResponse[T] } // JsonBaseResponse writes v into w with http.StatusOK. func JsonBaseResponse(w http.ResponseWriter, v any) { httpx.OkJson(w, wrapBaseResponse(v)) } // JsonBaseResponseCtx writes v into w with http.StatusOK. func JsonBaseResponseCtx(ctx context.Context, w http.ResponseWriter, v any) { httpx.OkJsonCtx(ctx, w, wrapBaseResponse(v)) }
通过源码得知,我们可以利用 JsonBaseResponse
或者 JsonBaseResponseCtx
函数,解析来修改模板。
- 在模板中引入
x/http
包xhttp "github.com/zeromicro/x/http"
- 替换响应体代码块内容为
xhttp.JsonBaseResponseCtx
即可
修改后的模板内容如下(粗体内容为新增模板内容):
package {{.PkgName}} import ( "net/http" "github.com/zeromicro/go-zero/rest/httpx" xhttp "github.com/zeromicro/x/http" {{.ImportPackages}} ) {{if .HasDoc}}{{.Doc}}{{end}} func {{.HandlerName}}(svcCtx *svc.ServiceContext) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { {{if .HasRequest}}var req types.{{.RequestType}} if err := httpx.Parse(r, &req); err != nil { httpx.ErrorCtx(r.Context(), w, err) return } {{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx) {{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}}) if err != nil { // httpx.ErrorCtx(r.Context(), w, err) xhttp.JsonBaseResponseCtx(r.Context(), w, err) } else { // {{if .HasResp}}httpx.OkJsonCtx(r.Context(), w, resp){{else}}httpx.Ok(w){{end}} {{if .HasResp}}xhttp.JsonBaseResponseCtx(r.Context(), w, resp){{else}}xhttp.JsonBaseResponseCtx(r.Context(), w, nil){{end}} } } }
实践2:增加 model 分页查询
在 goctl
内置模板中,考虑到分页不是所有业务都需要的查询,因此没有集成到内置模板中,但是这导致需要使用分页的研发人员没法得到满足,因此可以通过定制化模板来实现。这部分内容可以参考往期文章 goctl 模板分享|model 生成带 ListAll、ListByPage、BatchInsert 模板
3.4 模板定制化后怎么引用
模板定制化修改模板文件的方式有 3 种
- 直接修改默认
goctl
模板目录中的模板 - 自定义模板目录
- 从
git
仓库获取
3.4.1 直接修改默认 goctl 模板目录中的模板
goctl
模板默认模板为 ~/.goctl/${goctl 版本}
下,goctl
版本号获取为 goctl --version | awk '{print $3}'
,如我当前 goctl
版本为 goctl version 1.6.7 darwin/arm64
,因此默认模板就在 ~/.goctl/1.6.7
目录下。
优点:不需要每次在生成代码时通过 --home
来指定模板目录
缺点:会影响默认模板,不能进行模板隔离,生成不同业务的代码时会受影响,升级 goctl
版本后定制化模板无法跟着走。
3.4.2 自定义模板目录
自定义模板目录可以通过在模板初始化时指定一个目录来隔离 goctl template init --home $dir
,在使用的时候通过 --home
指定此前模板的目录即可。
优点:业务隔离,不用担心每次升级带来的版本隔离问题,可以统一使用一个模板目录
缺点:每次生成代码都需要通过 --home
来指定模板目录。
3.4.3 从 git 仓库获取
如果模板定制化有团队一致需求,可以将模板统一放进 git
仓库,在代码生成时从 git
仓库统一获取。
如生成 go
代码时的指令:
$ goctl api go --help Generate go files for provided api in api file Usage: goctl api go [flags] Flags: --api string The api file --branch string The branch of the remote repo, it does work with --remote --dir string The target dir -h, --help help for go --home string The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority --remote string The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure --style string The file naming format, see [https://github.com/zeromicro/go-zero/blob/master/tools/goctl/config/readme.md] (default "gozero")
其中的 --remote
和 --branch
就是控制模板从 git
分支获取的参数
参数字段 | 参数类型 | 是否必填 | 默认值 | 参数说明 |
... | ... | ... | ... | ... |
home | string | NO | ${HOME}/.goctl |
本地模板文件目录 |
remote | string | NO | 空字符串 | 远程模板所在 git 仓库地址,当此字段传值时,优先级高于 home 字段值 |
... | ... | ... | ... | ... |