我的业务不一样,用 go-zero 怎么搞?

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 我的业务不一样,用 go-zero 怎么搞?

在面对复杂且千差万别的业务场景时,通过自定义模板,我们可以生成高效、可维护的代码。在本节中,我们将介绍如何在 goctl 中使用 text/template,并分享一些最佳实践。

近期,我们也将给出 goctl  模板参数规范,并在新版本提供支持,以便大家更好的根据业务需求进行定制。

1. 介绍

text/templateGo 语言标准库中的一个包,用于处理文本模板。通过使用模板,可以将逻辑与表现分离,使代码更加清晰和易于维护。goctl 是一个用于生成 Go 代码的工具,通过结合 text/template,我们可以实现高效的代码生成。

2. text/template 基础

text/template 包提供了丰富的功能来处理文本模板。以下是一些关键概念和用法:

2.1 定义模板

在定义模板时,我们使用双花括号 {{ }} 来表示模板动作。以下是一个简单的模板示例:

const tmpl = `Hello, {{.Name}}!`

2.2 解析和执行模板

我们可以使用 template.Newtemplate.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 dockergatewaykubemodelmongonewapirpc

指令的模板定制化。对应的模板文件树如下:

.
├── 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 源码的前提下操作模板文件的过程。

首先,模板定制化并非万能的,模板定制化可以:

  1. 引入外部 pkg,比如引入 http 的参数校验包
  2. 增加公用业务逻辑,如生成 code-msg 的数据格式封装
  3. 替换 goctl 内部一些生成逻辑,如定制化请求参数校验
  4. 丰富业务实现,如对 model 增加分页查询

模板定制化不能做什么?

  1. 新增模板文件,新增模板文件 goctl 不会识别
  2. 自定义模板变量,对于非 goctl 注入的模板变量,模板是不会识别的,因此渲染出来的代码肯定是不符合预期的。
  3. 不能从环境变量读取

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 函数,解析来修改模板。

  1. 在模板中引入 x/httpxhttp "github.com/zeromicro/x/http"
  2. 替换响应体代码块内容为 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 种

  1. 直接修改默认 goctl 模板目录中的模板
  2. 自定义模板目录
  3. 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 字段值
... ... ... ... ...

项目地址

https://github.com/zeromicro/go-zero

相关文章
|
存储 SQL 前端开发
Go业务系统开发总结
Go业务系统开发总结
140 0
|
28天前
|
SQL 中间件 关系型数据库
那些年,我们在Go中间件上踩过的坑
作者总结了过去在Go中间件上踩过的坑,这些坑也促进了阿里内部Go中间件的完善,希望大家学习本文后,不犯同样的错误。
|
3月前
|
关系型数据库 MySQL API
我用 go-zero 一周实现了一个中台系统
我用 go-zero 一周实现了一个中台系统
|
3月前
|
人工智能 编译器 Go
Go 哪里没有做好?Rob Pike 深刻反思了
Go 哪里没有做好?Rob Pike 深刻反思了
|
6月前
|
Java Linux Go
关于我想写一个Go系列的这件事
本文是Go语言专栏的开篇,作者sharkChili分享了他对Go语言的喜爱,并简要介绍了如何在Windows和Linux上搭建Go环境。文章包括下载安装包、解压、配置环境变量等步骤。此外,还展示了编写并运行第一个"Hello, sharkChili"的Go程序。最后提到了Go项目的`.gitignore`文件示例,并鼓励读者关注作者的公众号以获取更多Go语言相关的内容。
36 0
|
6月前
|
Kubernetes Go 数据库
分享48个Go源码,总有一款适合您
分享48个Go源码,总有一款适合您
205 0
|
Java 测试技术 Go
送给学Go或者转Go同学的一套编码规范
有没有 xd 们是从别的语言转 Go
181 0
送给学Go或者转Go同学的一套编码规范
|
Go
Go有意思小问题汇集
Go有意思小问题汇集
79 0
|
编译器 Go Windows
Go知识梳理
快速学习Go知识梳理
Go知识梳理
|
安全 机器人 测试技术
惨,给Go提的代码被批麻了
hello大家好,我是小楼。 不知道大家还记不记得我上次找到了一个Go的Benchmark执行会超时的Bug?就是这篇文章《我好像发现了一个Go的Bug?》。 之后我就向Go提交了一个PR进行修复,本想等着代码被Merge进去,以后也可以吹牛说自己是个Go的Contributor,但事情并不顺利,今天就来分享一下这次失败的代码提交。
146 0
惨,给Go提的代码被批麻了