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
。
※参考1:go-zero超时时间
代码:https://gitee.com/XiMuQi/go-zero-micro/tree/v1.0.1
go-zero微服务项目的超时时间有三处配置,具体的配置看代码。
关于2、3的区别:
已经有了在api
中注册发现 rpc
中的timeout
,这里会不会显得多余?作者感觉开发者这样的设计更加双向灵活。从rpc
角度我可以设置为0(即:永不过期),但是我api请求必须保证有一个超时时间节点。这样,我们也可以统一开发规范,将rpc
统一设置为0,api
层面在注册发现时则根据实际要求更改。
参考1:进阶指南
目录拆分是根据业务横向拆分,将一个系统拆分成多个子系统,每个子系统应拥有独立的持久化存储,缓存系统。 如一个商城系统需要有用户系统(user),商品管理系统(product),订单系统(order),购物车系统(cart),结算中心系统(pay),售后系统(afterSale)等组成。
每个系统在对外(api
)提供服务的同时,也会提供数据给其他子系统进行数据访问的接口(rpc
),因此每个子系统可以拆分成两个服务:api
和rpc
。除此之外,一个服务下还可能有其他更多服务类型,如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
参考1:model生成
参考1:api文件编写
参考1:业务编码
参考1:jwt鉴权
jwt:全称 json web token。
1 签发时需要配置鉴权的密钥,过期时间,以及实现签发鉴权的逻辑即可。
2 客户端在发送请求时,如果在header
中加入了jwt token
,go-zero
从jwt token
解析后会将生成token
时传入的key-value
原封不动的放在http.Request
的Context
中,因此我们可以通过Context
拿到jwt token
中传递的值。
3 鉴权的签发一般是在用户登录成功
后,每个后续请求将包括JWT
,从而允许用户访问该令牌允许的路由,服务和资源。
jwt 配置流程
Auth:
AccessSecret: $AccessSecret
AccessExpire: $AccessExpire
$AccessSecret
:生成jwt token的密钥,最简单的方式可以使用一个uuid值。
$AccessExpire
:jwt token有效期,单位:秒
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
}
}
jwt
中还可以额外放入别的信息,比如userId
、userName
等等,可根据情况添加。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))
}
参考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
客户端在登陆成功后,服务端生成并返回了jwt
给客户端,客户端在后续请求时需要在header
中加入jwt token
,go-zero
从jwt token
解析后会将生成token
时传入的key-value
原封不动的放在http.Request
的Context
中,因此我们可以通过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
}
参考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
}
}
}
}
参考1:rpc编写与调用
参考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,
}
}
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
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()
}
参考1:模板修改
参考2:template 指令
模板修改:就是统一返回的格式,不论是正常还是异常的。在4.8错误处理
的基础上进行。这里的改造分两种方式,两种方式主要体现在对err不为nil
的处理上:推荐方式2
。
因为是在上一小节已经加入了错误处理,这里在响应时也要准确的返回错误的Code
信息,而官方文档:修改handler模板 却将返回的Code替换为了 -1
,这显然是不对的。方式1和方式2均是改进后的。
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)
}
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)
}
}
}
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)
}
}
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)
}
}
参考:Nacos安装使用【Docker】
略,配置有点复杂,非必须。了解配置会修改即可。