博客 / 詳情

返回

Go-Zero自定義goctl實戰:定製化模板,加速你的微服務開發效率(四)

前言

上一篇文章帶你實現了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": ""
  }
}

模板自定義規則

  1. 在 goctl 提供的有效數據範圍內修改,即不支持外部變量
  2. 不支持新增模板文件
  3. 不支持變量修改

總結

本文介紹瞭如何使用Go-Zero的goctl工具進行自定義模板的實戰,並提供了一個具體的案例來演示定製化模板的過程。

如果你需要詳細的命令使用詳情,可以參考官方文檔中的相關內容。模板定製化 | go-zero Documentation

我將繼續更新Go-Zero系列文章,如果你對Go語言或者微服務感興趣,歡迎關注我,也歡迎直接私信我。

gozero&微服務交流羣

我將繼續更新Go-Zero系列文章,如果你對Go語言或者微服務感興趣,歡迎關注我,也歡迎直接私信我。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.