当前位置: 首页 > 工具软件 > go-zero > 使用案例 >

go-zero学习 — 进阶

从智志
2023-12-01

1 注意事项

1 本文简化了整体环节过程,只对重难点问题进行详细讲解,建议结合本文与官方文档
2 在使用时发现,goctl.exe v1.4.3生成的xxxhandler.go

		if err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
		} else {
			httpx.OkJsonCtx(r.Context(), w, resp)
		}

goctl.exe v1.4.2生成的xxxhandler.go

		if err != nil {
			httpx.Error(w, err)
		} else {
			httpx.OkJson(w, resp)
		}

这个会对返回的信息格式产生影响【即4.8和4.9】,所以暂不推荐将goctl.exe升级到 v1.4.3

※3 超时时间

※参考1go-zero超时时间
代码:https://gitee.com/XiMuQi/go-zero-micro/tree/v1.0.1

go-zero微服务项目的超时时间有三处配置,具体的配置看代码。

  1. web请求到api服务的超时时间
  2. api请求rpc服务的超时时间
  3. api请求到此rpc服务的超时时间

关于2、3的区别:
已经有了在api中注册发现 rpc中的timeout,这里会不会显得多余?作者感觉开发者这样的设计更加双向灵活。从rpc角度我可以设置为0(即:永不过期),但是我api请求必须保证有一个超时时间节点。这样,我们也可以统一开发规范,将rpc统一设置为0,api层面在注册发现时则根据实际要求更改。

4 进阶

参考1:进阶指南

4.1 目录拆分

目录拆分是根据业务横向拆分,将一个系统拆分成多个子系统,每个子系统应拥有独立的持久化存储,缓存系统。 如一个商城系统需要有用户系统(user),商品管理系统(product),订单系统(order),购物车系统(cart),结算中心系统(pay),售后系统(afterSale)等组成。

每个系统在对外(api)提供服务的同时,也会提供数据给其他子系统进行数据访问的接口(rpc),因此每个子系统可以拆分成两个服务:apirpc。除此之外,一个服务下还可能有其他更多服务类型,如rmq(消息处理系统),cron(定时任务系统),script(脚本)等。

可以将每个服务的公共部分抽出来放在一起,比如错误的封装,sql的model等。

完整工程目录结构示例

book // 工程名称
├── common // 通用库
│   ├── randx
│   └── stringx
├── go.mod
├── go.sum
└── service // 服务存放目录
    ├── afterSale
    │   ├── api
    │   └── model
    │   └── rpc
    ├── cart
    │   ├── api
    │   └── model
    │   └── rpc
    ├── order
    │   ├── api
    │   └── model
    │   └── rpc
    ├── pay
    │   ├── api
    │   └── model
    │   └── rpc
    ├── product
    │   ├── api
    │   └── model
    │   └── rpc
    └── user
        ├── api
        ├── cronjob
        ├── model
        ├── rmq
        ├── rpc
        └── script

4.2 model生成

参考1:model生成

4.3 api文件编写

参考1:api文件编写

4.4 业务编码

参考1:业务编码

4.5 jwt鉴权

参考1:jwt鉴权
jwt:全称 json web token。

1 签发时需要配置鉴权的密钥,过期时间,以及实现签发鉴权的逻辑即可。

2 客户端在发送请求时,如果在header中加入了jwt tokengo-zerojwt token解析后会将生成token时传入的key-value原封不动的放在http.RequestContext中,因此我们可以通过Context拿到jwt token中传递的值。

3 鉴权的签发一般是在用户登录成功后,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。


4.5.1 jwt鉴权的签发

jwt 配置流程

  1. yaml 配置
Auth:
  AccessSecret: $AccessSecret
  AccessExpire: $AccessExpire

$AccessSecret:生成jwt token的密钥,最简单的方式可以使用一个uuid值。
$AccessExpire:jwt token有效期,单位:秒

  1. Config 配置
package config

import (
	"github.com/zeromicro/go-zero/core/stores/cache"
	"github.com/zeromicro/go-zero/rest"
)

type Config struct {
	rest.RestConf

	Mysql struct {
		DataSource string
	}

	CacheRedis cache.CacheConf

	Auth struct {
		AccessSecret string
		AccessExpire int64
	}
}
  1. 签发鉴权的逻辑:在登陆成功后签发。
    注意:除了当前时间,过期时间,密钥外,jwt中还可以额外放入别的信息,比如userIduserName等等,可根据情况添加。
package logic

import (
	"book/common/errorx"
	"book/service/user/sql/model"
	"context"
	"github.com/golang-jwt/jwt/v4"
	"strings"
	"time"

	"book/service/user/api/internal/svc"
	"book/service/user/api/internal/types"

	"github.com/zeromicro/go-zero/core/logx"
)

type LoginLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
	return &LoginLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginReply, error) {
	if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
		return nil, errorx.NewDefaultError(errorx.AccountErrorCode)
	}

	userInfo, err := l.svcCtx.UserModel.FindOneByNumber(l.ctx,req.Username)
	switch err {
	case nil:
	case model.ErrNotFound:
		return nil, errorx.NewDefaultError(errorx.UserIdErrorCode)
	default:
		return nil, err
	}

	if userInfo.Password != req.Password {
		return nil, errorx.NewDefaultError(errorx.PasswordErrorCode)
	}

	now := time.Now().Unix()
	accessExpire := l.svcCtx.Config.Auth.AccessExpire
	jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
	if err != nil {
		return nil, err
	}

	return &types.LoginReply{
		Id:           userInfo.Id,
		Name:         userInfo.Name,
		Gender:       userInfo.Gender,
		AccessToken:  jwtToken,
		AccessExpire: now + accessExpire,
		RefreshAfter: now + accessExpire/2,
	}, nil
}

func (l *LoginLogic) getJwtToken(secretKey string, iat, seconds, userId int64) (string, error) {
	claims := make(jwt.MapClaims)
	claims["exp"] = iat + seconds
	claims["iat"] = iat
	claims["userId"] = userId
	token := jwt.New(jwt.SigningMethodHS256)
	token.Claims = claims
	return token.SignedString([]byte(secretKey))
}

※4.5.2 使用jwt token鉴权的配置

参考1:search api使用jwt token鉴权

type (
    SearchReq {
        // 图书名称
        Name string `form:"name"`
    }

    SearchReply {
        Name string `json:"name"`
        Count int `json:"count"`
    }
)

@server(
    jwt: Auth
)
service search-api {
    @handler search
    get /search/do (SearchReq) returns (SearchReply)
}

//不需要jwt鉴权的路由
service search-api {
    @handler ping
    get /search/ping
}

注意:不需要jwt鉴权的路由可以应用在浏览/下载多媒体文件的请求中。

jwt: Auth:开启jwt鉴权
如果路由需要jwt鉴权,则需要在service上方声明此语法标志,如上文中的 /search/do
不需要jwt鉴权的路由就无需声明,如上文中/search/ping

4.5.3 jwt token 验证

客户端在登陆成功后,服务端生成并返回了jwt给客户端,客户端在后续请求时需要在header中加入jwt tokengo-zerojwt token解析后会将生成token时传入的key-value原封不动的放在http.RequestContext中,因此我们可以通过Context拿到jwt token中传递的值。

前提是要在xxx.api中加入jwt的拦截配置

syntax = "v1"

info(
	title: "type title here"
	desc: "type desc here"
	author: "type author here"
	email: "type email here"
	version: "type version here"
)

type (
	SearchReq {
		// 图书名称
		Name string `form:"name"`
	}

	SearchReply {
		Name  string `json:"name"`
		Count int    `json:"count"`
	}
)

@server(
	jwt: Auth
)
service search-api {
	@handler search
	get /search/do (SearchReq) returns (SearchReply)
}

service search-api {
	@handler ping
	get /search/ping
}

示例:
xxxlogic.go中添加一个log来输出从jwt解析出来的userId

func (l *SearchLogic) Search(req *types.SearchReq) (*types.SearchReply, error) {
    logx.Infof("userId: %v",l.ctx.Value("userId"))// 这里的key和生成jwt token时传入的key一致
    return &types.SearchReply{}, nil
}

※4.6 中间件使用

参考1:中间件使用

中间件分类
go-zero中,中间件可以分为路由【局部】中间件全局中间件路由【局部】中间件是指某一些特定路由需要实现中间件逻辑,其和jwt类似,没有放在jwt:xxx下的路由不会使用中间件功能, 而全局中间件的服务范围则是整个服务。

局部中间件全局中间件的讲解看参考1即可。

在中间件里调用其它服务
以调用Redis服务为例:

package middleware

import (
	"dsms-admin/api/internal/common/errorx"
	"encoding/json"
	"fmt"
	"github.com/zeromicro/go-zero/core/logx"
	"github.com/zeromicro/go-zero/core/stores/redis"
	"github.com/zeromicro/go-zero/rest/httpx"
	"net/http"
	"strings"
)

type CheckUrlMiddleware struct {
	Redis *redis.Redis
}

func NewCheckUrlMiddleware(Redis *redis.Redis) *CheckUrlMiddleware {
	return &CheckUrlMiddleware{Redis: Redis}
}

func (m *CheckUrlMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {

		//判断请求header中是否携带了x-user-id
		userId := r.Context().Value("userId").(json.Number).String()
		if userId == "" {
			logx.Errorf("缺少必要参数x-user-id")
			httpx.Error(w, errorx.NewDefaultError("缺少必要参数x-user-id"))
			return
		}

		if r.RequestURI == "/api/sys/user/currentUser" || r.RequestURI == "/api/sys/user/selectAllData" || r.RequestURI == "/api/sys/role/queryMenuByRoleId" {
			logx.Infof("用户userId: %s,访问: %s路径", userId, r.RequestURI)
			next(w, r)
		} else {
			//获取用户能访问的url
			urls, err := m.Redis.Get(userId)
			if err != nil {
				logx.Errorf("用户:%s,获取redis连接异常", userId)
				httpx.Error(w, errorx.NewDefaultError(fmt.Sprintf("用户:%s,获取redis连接异常", userId)))
				return
			}

			if len(strings.TrimSpace(urls)) == 0 {
				logx.Errorf("用户userId: %s,还没有登录", userId)
				httpx.Error(w, errorx.NewDefaultError(fmt.Sprintf("用户userId: %s,还没有登录,请先登录", userId)))
				return
			}

			backUrls := strings.Split(urls, ",")

			b := false
			for _, url := range backUrls {
				if url == r.RequestURI {
					b = true
					break
				}
			}

			if true || b { //todo delete
				logx.Infof("用户userId: %s,访问: %s路径", userId, r.RequestURI)
				next(w, r)
			} else {
				logx.Errorf("用户userId: %s,没有访问: %s路径的权限", userId, r.RequestURI)
				httpx.Error(w, errorx.NewDefaultError(fmt.Sprintf("用户userId: %s,没有访问: %s,路径的的权限,请联系管理员", userId, r.RequestURI)))
				return
			}

		}

	}
}

4.7 rpc编写与调用

参考1:rpc编写与调用

※4.8 错误处理

参考1:错误处理
错误处理是统一封装全局的错误返回信息。

自定义错误返回信息

  1. common中添加一个baseerror.go文件,并填入代码
package errorx

const defaultCode = 1001

type CodeError struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

type CodeErrorResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
}

func NewCodeError(code int, msg string) error {
	return &CodeError{Code: code, Msg: msg}
}

//func NewDefaultError(msg string) error {
//	return NewCodeError(defaultCode, msg)
//}

func NewDefaultError(code int) error {
	return NewCodeError(code, MapErrMsg(code))
}

func (e *CodeError) Error() string {
	return e.Msg
}

func (e *CodeError) Data() *CodeErrorResponse {
	return &CodeErrorResponse{
		Code: e.Code,
		Msg:  e.Msg,
	}
}

  1. xxxlogic.go 文件中逻辑错误用CodeError自定义错误替换
if len(strings.TrimSpace(req.Username)) == 0 || len(strings.TrimSpace(req.Password)) == 0 {
        return nil, errorx.NewDefaultError("参数错误")
    }

    userInfo, err := l.svcCtx.UserModel.FindOneByNumber(req.Username)
    switch err {
    case nil:
    case model.ErrNotFound:
        return nil, errorx.NewDefaultError("用户名不存在")
    default:
        return nil, err
    }

    if userInfo.Password != req.Password {
        return nil, errorx.NewDefaultError("用户密码不正确")
    }

    now := time.Now().Unix()
    accessExpire := l.svcCtx.Config.Auth.AccessExpire
    jwtToken, err := l.getJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, l.svcCtx.Config.Auth.AccessExpire, userInfo.Id)
    if err != nil {
        return nil, err
    }

    return &types.LoginReply{
        Id:           userInfo.Id,
        Name:         userInfo.Name,
        Gender:       userInfo.Gender,
        AccessToken:  jwtToken,
        AccessExpire: now + accessExpire,
        RefreshAfter: now + accessExpire/2,
    }, nil
  1. 在启动类中加入自定义错误的拦截
func main() {
    flag.Parse()

    var c config.Config
    conf.MustLoad(*configFile, &c)

    ctx := svc.NewServiceContext(c)
    server := rest.MustNewServer(c.RestConf)
    defer server.Stop()

    handler.RegisterHandlers(server, ctx)

    // 自定义错误
    httpx.SetErrorHandler(func(err error) (int, interface{}) {
        switch e := err.(type) {
        case *errorx.CodeError:
            return http.StatusOK, e.Data()
        default:
            return http.StatusInternalServerError, nil
        }
    })

    fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
    server.Start()
}

4.9 模板修改

参考1:模板修改
参考2:template 指令

模板修改:就是统一返回的格式,不论是正常还是异常的。在4.8错误处理的基础上进行。这里的改造分两种方式,两种方式主要体现在对err不为nil的处理上:推荐方式2

因为是在上一小节已经加入了错误处理,这里在响应时也要准确的返回错误的Code信息,而官方文档:修改handler模板 却将返回的Code替换为了 -1 ,这显然是不对的。方式1和方式2均是改进后的。

4.9.1 方式1

  1. 新建响应体封装文件
package response

import (
	"github.com/zeromicro/go-zero/rest/httpx"
	"net/http"
)

type Body struct {
	Code int         `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data,omitempty"`
}

func Response(w http.ResponseWriter, resp interface{}, err error) {
	var body Body
	if err != nil {
		body.Code = 0
		body.Msg = err.Error()
	} else {
		body.Code = 200
		body.Msg = "success"
		body.Data = resp
	}
	httpx.OkJson(w, body)
}
  1. 修改 xxxhandler.go 文件
package handler

import (
	"go-zero-micro/common/response"
	"net/http"

	"github.com/zeromicro/go-zero/rest/httpx"
	"go-zero-micro/api/order/internal/logic"
	"go-zero-micro/api/order/internal/svc"
	"go-zero-micro/api/order/internal/types"
)

func getOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.OrderReq
		if err := httpx.Parse(r, &req); err != nil {
			httpx.Error(w, err)
			return
		}

		l := logic.NewGetOrderLogic(r.Context(), svcCtx)
		resp, err := l.GetOrder(&req)
		if err != nil {
			httpx.Error(w, err)
		} else {
			//httpx.OkJson(w, resp)
			response.Response(w, resp, err)
		}
	}
}

4.9.2 方式2

  1. 新建响应体封装文件
package response

import (
	"github.com/zeromicro/go-zero/rest/httpx"
	"net/http"
)

type Body struct {
	Code int         `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data,omitempty"`
}

func Response(w http.ResponseWriter, resp interface{}, err error) {
	//var body Body
	//if err != nil {
	//	body.Code = 0
	//	body.Msg = err.Error()
	//} else {
	//	body.Code = 200
	//	body.Msg = "success"
	//	body.Data = resp
	//}
	//httpx.OkJson(w, body)

	if err != nil {
		httpx.Error(w, err)
	} else {
		var body Body
		body.Code = 200
		body.Msg = "success"
		body.Data = resp
		httpx.OkJson(w, body)
	}
}
  1. 修改 xxxhandler.go 文件
package handler

import (
	"go-zero-micro/common/response"
	"net/http"

	"github.com/zeromicro/go-zero/rest/httpx"
	"go-zero-micro/api/order/internal/logic"
	"go-zero-micro/api/order/internal/svc"
	"go-zero-micro/api/order/internal/types"
)

func getOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req types.OrderReq
		if err := httpx.Parse(r, &req); err != nil {
			httpx.Error(w, err)
			return
		}

		l := logic.NewGetOrderLogic(r.Context(), svcCtx)
		resp, err := l.GetOrder(&req)
		//if err != nil {
		//	httpx.Error(w, err)
		//} else {
		//	//httpx.OkJson(w, resp)
		//	response.Response(w, resp, err)
		//}
		response.Response(w, resp, err)
	}
}

5 使用Nacos

5.1 Nacos服务搭建

参考:Nacos安装使用【Docker】

5.2 go-zero使用Nacos

略,配置有点复杂,非必须。了解配置会修改即可。

 类似资料: