go-zero学习 — 进阶
创始人
2025-05-31 14:57:54
0

go-zero学习 — 进阶

  • 1 注意事项
  • ※3 超时时间
  • 4 进阶
    • 4.1 目录拆分
    • 4.2 model生成
    • 4.3 api文件编写
    • 4.4 业务编码
    • 4.5 jwt鉴权
      • 4.5.1 jwt鉴权的签发
      • ※4.5.2 使用jwt token鉴权的配置
      • 4.5.3 jwt token 验证
    • ※4.6 中间件使用
    • 4.7 rpc编写与调用
    • ※4.8 错误处理
    • 4.9 模板修改
      • 4.9.1 方式1
      • 4.9.2 方式2
  • 5 使用Nacos
    • 5.1 Nacos服务搭建
    • 5.2 go-zero使用Nacos

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 超时时间

※参考1:go-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: $AccessSecretAccessExpire: $AccessExpire

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

  1. Config 配置
package configimport ("github.com/zeromicro/go-zero/core/stores/cache""github.com/zeromicro/go-zero/rest"
)type Config struct {rest.RestConfMysql struct {DataSource string}CacheRedis cache.CacheConfAuth struct {AccessSecret stringAccessExpire int64}
}
  1. 签发鉴权的逻辑:在登陆成功后签发。
    注意:除了当前时间,过期时间,密钥外,jwt中还可以额外放入别的信息,比如userIduserName等等,可根据情况添加。
package logicimport ("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.Loggerctx    context.ContextsvcCtx *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.AccessExpirejwtToken, 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 + secondsclaims["iat"] = iatclaims["userId"] = userIdtoken := jwt.New(jwt.SigningMethodHS256)token.Claims = claimsreturn 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 searchget /search/do (SearchReq) returns (SearchReply)
}//不需要jwt鉴权的路由
service search-api {@handler pingget /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 searchget /search/do (SearchReq) returns (SearchReply)
}service search-api {@handler pingget /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 middlewareimport ("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-iduserId := 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 {//获取用户能访问的urlurls, 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 := falsefor _, url := range backUrls {if url == r.RequestURI {b = truebreak}}if true || b { //todo deletelogx.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 errorxconst defaultCode = 1001type 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.AccessExpirejwtToken, 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.Configconf.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 responseimport ("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 Bodyif err != nil {body.Code = 0body.Msg = err.Error()} else {body.Code = 200body.Msg = "success"body.Data = resp}httpx.OkJson(w, body)
}
  1. 修改 xxxhandler.go 文件
package handlerimport ("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.OrderReqif 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 responseimport ("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 Bodybody.Code = 200body.Msg = "success"body.Data = resphttpx.OkJson(w, body)}
}
  1. 修改 xxxhandler.go 文件
package handlerimport ("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.OrderReqif 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

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

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...