前言
上一篇文章帶你實現了Go-Zero和goctl:解鎖微服務開發的神器,快速上手指南,本文將繼續深入探討Go-Zero的強大之處,並介紹如何使用goctl工具實現模板定製化,並根據實際項目業務需求進行模板定製化實現。
通過本文的教程,你能夠親自實踐並完成goctl模板的定製化,進一步提升你的Go-Zero開發技能。
概述
goctl 代碼生成是基於 go 的模板去實現數據驅動的,默認情況會選擇內存中的模板進行生成,當開發需要修改模板時,就需要定製化模板,goctl為我們實現了這一功能。
實戰前準備
首先需要你在本地安裝goctl、protoc、go-zero,下載教學和地址點擊這裏,按照教程操作即可,非常簡單。
下面按順序和我操作吧,對整體開發流程不清楚的同學務必先看我前篇文章:GoZero的開發技巧 & 整體開發流程
本文重在實戰,如果對goctl毫不瞭解的話,建議先看我前一篇文章:Go-Zero和goctl:解鎖微服務開發的神器,快速上手指南
以下均以我的商業項目舉例,應該對你有啓發:
(後面我會把商業項目脱敏開源出來,歡迎關注我)
數據表生成Model方法腳本
首先在deploy下新增script目錄,結構如下圖所示。
腳本內容如下:
#!/usr/bin/env bash
# 使用方法:
# ./genModel.sh lottery lottery
# ./genModel.sh lottery prize
# 再將./genModel下的文件剪切到對應服務的model目錄裏面,記得改package
#生成的表名
tables=$2
#表生成的genmodel目錄
modeldir=./genModel
# 數據庫配置
host=127.0.0.1
port=33069
dbname=$1
username=root
passwd=PXDN93VRKUm8TeE7
template=../../goctl/1.6.1
echo "開始創建庫:$dbname 的表:$2"
goctl model mysql datasource -url="${username}:${passwd}@tcp(${host}:${port})/${dbname}" -table="${tables}" -dir="${modeldir}" -cache=true --home="${template}" --style=goZero
模板定製化使用方法
相關命令使用詳情,參考:官網文檔
具體使用方法網上有很多文章介紹,官網也有詳細步驟。這裏更加註重商業項目對於模板定製化的實戰,對相關操作不進行贅述,快速過一遍流程即可。
初始化模板到本地
依據前文所介紹的項目目錄結構,我們將自定義模板放在deploy下面即可,並且採用的版本號為1.6.1(目錄路徑根據自己實際情況修改)
goctl template init --home $HOME/Desktop/lottery-backend/deploy/goctl/1.6.1
注意:如果不指定–home 他會初始化到$HOME/.goctl
這樣就生成好自己版本的goctl模板啦,可以根據自己的實際需求進行模板的修改。
接下來分享我們項目中關於自定義goctl的實戰。
自定義goctl實戰
實戰1:Model層方法定製化
很多時候我們需要對數據進行分頁查詢。這個方法是一個通用的方法,可以在很多地方複用,所以放入模板去生成,這樣可以減少重複代碼,提高開發效率。
步驟一:在model/update.tpl下面新增一個方法FindPageListByPage
方法具體實現如下
func (m *default{{.upperStartCamelObject}}Model) FindPageListByPage(ctx context.Context,builder squirrel.SelectBuilder,page ,pageSize int64,orderBy string) ([]*{{.upperStartCamelObject}},error) {
builder = builder.Columns({{.lowerStartCamelObject}}Rows)
if orderBy == ""{
builder = builder.OrderBy("id DESC")
}else{
builder = builder.OrderBy(orderBy)
}
if page < 1{
page = 1
}
offset := (page - 1) * pageSize
query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql()
if err != nil {
return nil, err
}
var resp []*{{.upperStartCamelObject}}
{{if .withCache}}err = m.QueryRowsNoCacheCtx(ctx,&resp, query, values...){{else}}
err = m.conn.QueryRowsCtx(ctx,&resp, query, values...)
{{end}}
switch err {
case nil:
return resp, nil
default:
return nil, err
}
}
步驟二:使用之前做好的腳本生成代碼
使用GitBash打開deploy/script/mysql目錄,執行腳本
此時genModel目錄下面就會生成相關代碼
步驟三:將生成的代碼剪切到項目目錄的對應位置
效果
默認模板生成的Model層方法
自定義模板生成的Model層方法
生成的FindPageListByPage方法
func (m *defaultLotteryModel) FindPageListByPage(ctx context.Context, builder squirrel.SelectBuilder, page, pageSize int64, orderBy string) ([]*Lottery, error) {
builder = builder.Columns(lotteryRows)
if orderBy == "" {
builder = builder.OrderBy("id DESC")
} else {
builder = builder.OrderBy(orderBy)
}
if page < 1 {
page = 1
}
offset := (page - 1) * pageSize
query, values, err := builder.Where("del_state = ?", globalkey.DelStateNo).Offset(uint64(offset)).Limit(uint64(pageSize)).ToSql()
if err != nil {
return nil, err
}
var resp []*Lottery
err = m.QueryRowsNoCacheCtx(ctx, &resp, query, values...)
switch err {
case nil:
return resp, nil
default:
return nil, err
}
}
實戰2:api自定義響應返回以及集成validator庫校驗參數
當我們希望自定義統一返回響應體以及希望每個api接口都進行參數校驗時,我們可以在模板中修改handler層的代碼,從而實現這些效果。
步驟一:實現自定義統一返回響應
在common目錄下新建result目錄和httpResult.go文件,如下圖所示
具體實現代碼不是本文重點,下面是提供的代碼
package result
import (
"fmt"
"net/http"
"looklook/common/xerr"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest/httpx"
"google.golang.org/grpc/status"
)
// http返回
func HttpResult(r *http.Request, w http.ResponseWriter, resp interface{}, err error) {
if err == nil {
//成功返回
r := Success(resp)
httpx.WriteJson(w, http.StatusOK, r)
} else {
//錯誤返回
errcode := xerr.SERVER_COMMON_ERROR
errmsg := "服務器開小差啦,稍後再來試一試"
causeErr := errors.Cause(err) // err類型
if e, ok := causeErr.(*xerr.CodeError); ok { //自定義錯誤類型
//自定義CodeError
errcode = e.GetErrCode()
errmsg = e.GetErrMsg()
} else {
if gstatus, ok := status.FromError(causeErr); ok { // grpc err錯誤
grpcCode := uint32(gstatus.Code())
if xerr.IsCodeErr(grpcCode) { //區分自定義錯誤跟系統底層、db等錯誤,底層、db錯誤不能返回給前端
errcode = grpcCode
errmsg = gstatus.Message()
}
}
}
logx.WithContext(r.Context()).Errorf("【API-ERR】 : %+v ", err)
httpx.WriteJson(w, http.StatusBadRequest, Error(errcode, errmsg))
}
}
// http 參數錯誤返回
func ParamErrorResult(r *http.Request, w http.ResponseWriter, err error) {
errMsg := fmt.Sprintf("%s ,%s", xerr.MapErrMsg(xerr.REUQEST_PARAM_ERROR), err.Error())
httpx.WriteJson(w, http.StatusBadRequest, Error(xerr.REUQEST_PARAM_ERROR, errMsg))
}
步驟二:在handler下面引入定製的validator包
關於定製validator也不是本文重點,感興趣的同學可以關注我,留言。
package translator
import (
"errors"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
"looklook/app/lottery/cmd/api/internal/logic/lottery"
"looklook/app/lottery/cmd/api/internal/types"
"reflect"
"strings"
)
func Validate(dataStruct interface{}) error {
zh_ch := zh.New()
validate := validator.New()
// 註冊一個函數,獲取struct tag裏自定義的label作為字段名
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
// 在這裏註冊自定義結構體/字段校驗方法
// 註冊自定義結構體校驗方法
validate.RegisterStructValidation(lottery.SignUpParamStructLevelValidation, types.TestReq{})
// 註冊自定義結構體字段校驗方法
if err := validate.RegisterValidation("checkDate", lottery.CheckDate); err != nil {
return err
}
uni := ut.New(zh_ch)
trans, _ := uni.GetTranslator("zh")
// 在這裏註冊自定義tag翻譯
// 注意!因為這裏會使用到trans實例
// 所以這一步註冊要放到trans初始化的後面
if err := validate.RegisterTranslation(
"checkDate",
trans,
registerTranslator("checkDate", "{0}必須要晚於當前日期"),
translate,
); err != nil {
return err
}
// 驗證器註冊翻譯器
zh_translations.RegisterDefaultTranslations(validate, trans)
err := validate.Struct(dataStruct)
if err != nil {
for _, err := range err.(validator.ValidationErrors) {
return errors.New(err.Translate(trans))
}
}
return nil
}
// registerTranslator 為自定義字段添加翻譯功能
func registerTranslator(tag string, msg string) validator.RegisterTranslationsFunc {
return func(trans ut.Translator) error {
if err := trans.Add(tag, msg, false); err != nil {
return err
}
return nil
}
}
// translate 自定義字段的翻譯方法
func translate(trans ut.Translator, fe validator.FieldError) string {
msg, err := trans.T(fe.Tag(), fe.Field())
if err != nil {
panic(fe.(error).Error())
}
return msg
}
步驟三:修改handler.tpl模板代碼
將模板替換為以下內容
package {{.PkgName}}
import (
"net/http"
"looklook/common/result"
"github.com/zeromicro/go-zero/rest/httpx"
{{.ImportPackages}}
)
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
}
validateErr := translator.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(r, w, validateErr)
return
}
{{end}}l := {{.LogicName}}.New{{.LogicType}}(r.Context(), svcCtx)
{{if .HasResp}}resp, {{end}}err := l.{{.Call}}({{if .HasRequest}}&req{{end}})
result.HttpResult(r, w, {{if .HasResp}}resp{{else}}nil{{end}}, err)
}
}
步驟四:生成對應的代碼
注意生成handler後需要手動點開生成的handler文件,導入translator包,否則服務會報錯!!!
# 使用自定義的goctl 生成api
goctl api go -api main.api -dir ../ --style=goZero --home=../../../../../deploy/goctl/1.6.1
修改後的響應體
{
"code": 200,
"msg": "OK",
"data": {
"message": ""
}
}
模板自定義規則
- 在 goctl 提供的有效數據範圍內修改,即不支持外部變量
- 不支持新增模板文件
- 不支持變量修改
總結
本文介紹瞭如何使用Go-Zero的goctl工具進行自定義模板的實戰,並提供了一個具體的案例來演示定製化模板的過程。
如果你需要詳細的命令使用詳情,可以參考官方文檔中的相關內容。模板定製化 | go-zero Documentation
我將繼續更新Go-Zero系列文章,如果你對Go語言或者微服務感興趣,歡迎關注我,也歡迎直接私信我。
gozero&微服務交流羣
我將繼續更新Go-Zero系列文章,如果你對Go語言或者微服務感興趣,歡迎關注我,也歡迎直接私信我。