前言
上一篇文章介绍了如何配置一个go-zero + gorm的开发框架,并演示创建和查询用户。本文介绍如何创建go-zero API项目,调用编写的RPC服务提供注册和登录功能,并且实现JWT鉴权
定义数据结构
.api文件是go-zero自创的文件格式,和protobuf的语法有很大不同,但好在不难理解。具体语法介绍可看官网文档:go-zero.dev/docs/tasks/...
这里我打算先编写接口定义,然后直接一次性使用goctl生成代码。api文件支持将一个数据结构嵌入另一个结构中,便于编写统一的响应格式。
首先定义空请求结构和基础的响应结构:
go
syntax = "v1"
type Empty {
}
type BasicResponse {
StatusCode int32 `json:"status_code"`
StatusMsg string `json:"status_msg"`
}
type关键字也支持代码块,只需要用type()
包裹,就可以省去关键词。下面定义了注册和登录的请求和响应结构。
go
type (
RegisterRequest struct {
Username string `form:"username"`
Password string `form:"password"`
}
RegisterResponse struct {
BasicResponse
UserId int64 `json:"user_id"`
Token string `json:"token"`
}
)
type (
LoginRequest struct {
Username string `form:"username"`
Password string `form:"password"`
}
LoginResponse struct {
BasicResponse
UserId int64 `json:"user_id"`
Token string `json:"token"`
}
)
最后还要定义一个获取用户信息的接口。因为本文不涉及创作、社交方面的业务逻辑实现,所以User
结构其实没什么可以返回的信息。之后将其嵌入响应类型的user
字段中:
go
type (
User {
Id int64 `json:"id"`
Name string `json:"name"`
}
GetUserInfoRequest struct {
UserId int64 `form:"user_id"`
Token string `form:"token"`
}
GetUserInfoResponse struct {
BasicResponse
User User `json:"user"`
}
)
定义接口路由
先不考虑鉴权问题,把所有api路由配置好,这里的请求路径参考了青训营给出的要求:
less
@server(
group: app
)
service app {
@handler Ping
get /ping (Empty) returns (BasicResponse)
}
@server(
group: user
prefix: /douyin/user
)
service app {
@handler Register
post /register (RegisterRequest) returns (RegisterResponse)
@handler Login
post /login (LoginRequest) returns (LoginResponse)
@handler GetUserInfo
get / (GetUserInfoRequest) returns (GetUserInfoResponse)
}
这段配置有以下几个要点:
- 如果service代码块有多个,要求后面的名称(即"app")相同
- 形如装饰器的@server代码块可以做代码分组、路由前缀、鉴权、中间件等多种配置。
- 这里把路由分成app和user两组,之后生成时会分组放在文件夹里
- 给user组设置了
/douyin/user
的路由器前缀,就不用重复写了 - 鉴权和中间件配置下文会提到
- 每个接口需要一个
handler
,其实就是给函数起名 - 接口定义的格式是
<HTTP方法> <子路径> (<请求数据结构>) returns (<响应数据结构>)
创建项目模板
这次不是新建示例项目,而是根据现有的api声明创建,所以要运行:
shell
goctl api go --api app.api --dir=. --style=goZero
这会在当前目录生成项目文件。之后进入目录安装依赖:
shell
go mod tidy
测试Ping接口
Ping接口的逻辑在internal/logic/app/pingLogic.go
,打开文件并编辑handler函数:
go
func (l *PingLogic) Ping(req *types.Empty) (resp *types.BasicResponse, err error) {
return &types.BasicResponse{
StatusCode: 0,
StatusMsg: "pong",
}, nil
}
这里的写法和rpc微服务有点区别。注意到传入的resp
是一个指针,所以需要手动创建一个响应结构体并设置键值。
这样就算完成Ping接口了,可以运行服务,在浏览器里访问这个地址来测试:
配置RPC客户端连接
扩展config数据结构
给internal/config/config.go
增加一行UserRPC
配置项,这里要引用zrpc.RpcClientConf
这个数据类型
go
package config
import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
rest.RestConf
UserRpc zrpc.RpcClientConf
}
添加节点配置
给etc/app.yaml
增加UserRpc
的地址配置:
yaml
Name: app
Host: 0.0.0.0
Port: 8888
UserRpc:
Endpoints:
- 127.0.0.1:8080
NonBlock: true
虽然现在只有本地测试的一个节点,但是配置的时候我使用的是多节点直连模式,便于以后扩展节点数量。相关文档在此:go-zero.dev/docs/tutori...
这里的NonBlock配置,官方文档解释不清楚,我从一份学习笔记中找到了解释:
rpc的client的NonBlock选项默认为false,必须等依赖的rpc启动后自己才能启动,设置为true后可以不等依赖启动
oublie 《golang-go-zero-个人笔记》 oublie6.github.io/posts/golan...
修改go.mod
在使用代码生成编写RPC服务时,会一并生成调用结构对应的RpcClient,为了使用它,需要在本项目里调用RPC项目的userClient包。
目前,我的两个项目放在同一个文件夹里,需要在go.mod
中增加以下两条配置,首先引用包,然后声明引用的相对路径
java
module app
go 1.20
// ...
require user v0.0.0
replace user => ../user
//...
修改ServiceContext
修改ServiceContext的定义,添加UserRpc
一项。因为刚刚引用了本地包,所以这里需要import "user/userClient"
在初始化函数中创建客户端连接并返回得到的客户端,全部代码如下:
go
package svc
import (
"app/internal/config"
"user/userClient"
"github.com/zeromicro/go-zero/zrpc"
)
type ServiceContext struct {
Config config.Config
UserRpc userClient.User
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: userClient.NewUser(zrpc.MustNewClient(c.UserRpc)),
}
}
这样就完成了RPC客户端的配置和连接。
配置请求错误处理
定义公共错误类型
服务过程中可能遇到错误的用户请求,或者发生未知的内部错误,因此定义ApiError
和ServerError
两个错误类型。这部分内容是公用的,所以创建common/common.go
来存放。
go
package common
import (
"app/internal/types"
"fmt"
"regexp"
)
type ApiError struct {
StatusCode int
Code int32
Message string
}
type ServerError struct {
ApiError
Detail error
}
func (e ApiError) Error() string {
return fmt.Sprintf("(%d) %s", e.Code, e.Message)
}
func (e ApiError) Response() *types.BasicResponse {
return &types.BasicResponse{
StatusCode: e.Code,
StatusMsg: e.Message,
}
}
Go规定所有的的Error
类型都要实现Error()
接口,输出一段错误文本。
但是API在报错的时候应该向用户端返回结构化的错误信息,因此增加一个Response
方法,将错误信息填入BasicResponse
中返回
ServerError
会额外携带原本的error
结构体,用于跟踪错误
自定义错误处理函数
打开app.go
,在handler.RegisterHandlers(server, ctx)
后新增一行httpx.SetErrorHandler(errHandler)
,之后编写错误处理函数:
go
func errHandler(err error) (int, interface{}) {
switch e := err.(type) {
case common.ApiError:
return e.StatusCode, e.Response()
case common.ServerError:
fmt.Printf("%s: %s\n", e, e.Detail)
return e.StatusCode, e.Response()
default:
fmt.Printf("Internal Server Error: %s\n", e)
return http.StatusInternalServerError, &types.BasicResponse{
StatusCode: 50000,
StatusMsg: "Internal Server Error",
}
}
}
这里用到了Go的类型断言功能,根据进入的case分支不同,变量e会被断言到对应的数据结构上。
- 如果判断是
ApiError
,则返回自定义的状态码和响应即可 - 如果是
ServerError
,则额外打印一下错误信息后返回 - 如果是未知错误,则打印错误后返回标准的
http.StatusInternalServerError
和默认信息
完善RPC错误处理
暴露公共错误
这里需要修改上文编写的RPC组件,将一些标准化的错误暴露成公共变量。回到RPC微服务的user文件夹 ,创建common/errors.go
,将之前Logic文件中用status.Error创建的错误移动到这里:
go
package common
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
ErrUserAlreadyExists = status.New(codes.AlreadyExists, "user already exists")
ErrUserNotFound = status.New(codes.NotFound, "user not found")
)
注意 ,这里使用的是status.New
而非status.Error
。
gRPC status和error的关系
gRPC提供了gRPC status和error互相转换的方法。调用status.Status
上的Err()
方法,会把他转换成一个error
。
查看源码会发现status.Error()
函数就是先调用New()
然后立即链式调用Err()
转换成error
。
而想要把error
转换回status
时,则要调用FromError(err error) (s *Status, ok bool)
这个函数。可以查看文档得知其用法pkg.go.dev/google.gola...
在API侧,为了判断RPC服务错误的类型,需要将调用返回的err转换成status,然后调用Code()
或者Messgae()
方法获取原来的信息。
因此为了少做一步转换,这里的公共错误保留了status.Status类型。与此同时 ,这个类型还支持使用WithDetials()
函数附加自定义的错误信息,这个用法马上会讲到。
改写原来的错误处理
这里以createLogic.go
为例,详细介绍一下修改的方法。
已知错误类型处理
首先,用common.ErrUserAlreadyExists.Err()
替换掉原来当场新建的错误
go
if count > 0 {
return nil, common.ErrUserAlreadyExists.Err()
}
未知错误类型处理
对于未知的错误,没有办法给出固定的错误类型。但是这里要更正一下之前的错误 :status.Error()
接收的虽然是一个int32
的code,但是实际上gRPC团队对此有专门的定义,并不是随意设定的 。具体定义可见这里pkg.go.dev/google.gola...
gRPC的codes
包中预置了这些数值,因此将位置错误都改写为status.Error(codes.Internal, err.Error())
go
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
附加额外的错误信息
gRPC提供的status类型承载信息很有限,不能传递出确切的业务情况。
谷歌团队为此预留了一个Details
字段,用于传递自定义的信息。golang版本的gRPC库刚好有相关实现,因此我们可以使用WithDetails
函数来传入自定义的信息。
以err = l.svcCtx.DB.Create(newUser).Error
这句为例,我既想知道错误是在创建用户时发生的,又想知道错误的具体情况,因此先使用status.New(codes.Internal, "error creating user record")
创建一个标准status,之后再添加详细信息:
go
err = l.svcCtx.DB.Create(newUser).Error
if err != nil {
st, _ := status.New(codes.Internal, "error creating user record").WithDetails(
&any.Any{
Value: []byte(err.Error()),
})
return nil, st.Err()
}
这里使用了一个any.Any
类型的结构体,内部类型是anypb.Any
。any
包来源于github.com/golang/protobuf/ptypes/any
,IDE可能无法自动补全导入,需要手动设置一下。
这个包有一个Value
字段,用于存储用户自定义的信息。这里我就简单得获得错误信息转为[]bytes
后存入
WithDetails
支持传递多个Any
结构体,只需要将其作为二、三、四个参数输入即可,客户端在收到后需要遍历获取错误,马上会提到。
完善注册函数
现在回到API服务的app文件夹。这里以用户注册为例,介绍如何完成字段校验、RPC调用、错误处理、结果返回。
打开registerLogic.go
文件,先完成基础的逻辑,要对输入的用户名和密码使用正则表达式判断合规性
使用正则表达式校验字段
go
func (l *RegisterLogic) Register(req *types.RegisterRequest) (resp *types.RegisterResponse, err error) {
username := req.Username
password := req.Password
usernamePattern := "^[^\\s]{1,20}$"
passwordPattern := "^[!-~]{8,24}$"
if !utils.MatchRegexp(usernamePattern, username) || !utils.MatchRegexp(passwordPattern, password) {
return nil, utils.ApiError{
StatusCode: 422,
Code: 42201,
Message: "Invalid username or password",
}
}
这里编写了一个工具函数MatchRegexp
,判断给定的字符串是否符合给定的正则表达式,位于common/common.go
中
go
func MatchRegexp(pattern string, value string) bool {
r := regexp.MustCompile(pattern)
return r.MatchString(value)
}
调用微服务并处理错误
调用微服务
微服务的调用比较简单,因为有客户端预定义的方法,只要传入参数即可。请求传回的res是之前使用protobuf定义的CreateResponse
类型。
go
res, err := l.svcCtx.UserRpc.Create(l.ctx, &user.CreateRequest{
Username: username,
Password: password,
})
编写错误判断函数
接下来重点处理错误。这个接口在调用时可能返回用户已经存在 的错误,因此要检查返回错误是否是这一类型。这里先介绍错误判断函数 MatchError(error, *status.Status)
这个函数接收userClient返回的错误,以及之前在user/common
里定义的标准status,返回由err解析出的status,以及错误是否吻合。
go
func MatchError(err error, target *status.Status) (*status.Status, bool) {
st, _ := status.FromError(err)
if st.Message() == target.Message() {
return st, true
}
return st, false
}
注意 ,这里是不能用errors.Is
或者==
直接进行比较的,因为在网络传输过程中,RPC服务端和客户端的错误不在同一个内存地址,比较一定会返回false
,所以这里只能用Message()
或者Code()
做比较。
处理错误
之后用使用这个函数来区分ErrUserAlreadyExists
和未知错误。如果是用户已经存在,则返回正常的ApiError
通知用户;否则,使用Details()
函数获取所有的详细信息并打印出来,最后返回内部服务器错误并附上这个err
。
注意 ,这里给user/common
包起了一个别名userCommon
,否则会和app/common
冲突。应该考虑将两个包合并起来,统一使用。
这里也用到了类型断言 ,因为go认为Details()
返回的是[]interface{}
,需要显式转为*anypb.Any
否则内部字段未知。
go
if err != nil {
if st, match := common.MatchError(err, userCommon.ErrUserAlreadyExists); match {
return nil, common.ApiError{
StatusCode: 422,
Code: 42201,
Message: "用户名已被使用",
}
} else {
for index, item := range st.Details() {
detail := item.(*anypb.Any)
fmt.Printf("%d: %s\n", index, string(detail.Value))
}
return nil, common.ServerError{
ApiError: common.ApiError{
StatusCode: 500,
Code: 50000,
Message: "Internal Server Error",
},
Detail: err,
}
}
}
返回注册成功的消息
业务逻辑的最后就是返回注册消息了,这里的UserId
字段由res
携带,取出并赋值即可。Token
字段我们还未实现,因此留在这里占位:
go
return &types.RegisterResponse{
BasicResponse: types.BasicResponse{
StatusCode: 0,
StatusMsg: "",
},
UserId: res.UserId,
Token: "not implemented",
}, nil
加入JWT Token发放
下载软件包
软件的Github仓库地址在github.com/golang-jwt/...
现在已经推荐使用v5版本,于是安装:
shell
go get -u github.com/golang-jwt/jwt/v5
定义token结构体
创建intermal/utils/jwt.go
,我们将在里面编写JWT发放和验证的相关内容。
JWT token支持自定义载荷,可以携带自己想要的数据。这里我们只需携带UserId即可,因此定义一个UserClaims
结构体,并用jwt.RegisteredClaims
扩展出jwt的基础字段
go
package utils
import (
"github.com/golang-jwt/jwt/v5"
)
type UserClaims struct {
UserId int64 `json:"user_id"`
jwt.RegisteredClaims
}
注意 ,jwt.StandardClaims
在v5版本中已经没有了,如果使用v4版本的jwt会提示该类型被弃用 。因此本文使用推荐的jwt.RegisteredClaims
来替代
编写发放函数
CreateToken
函数用于在登录和注册的时候发放token。他根据给定的UserId,签名密钥和有效时长生成token并签名。通常用当前时间作为签发和生效时间,而过期时间需要即时相加计算一下。
这部分操作与官网的教程几乎一致,可以参考pkg.go.dev/github.com/...
注意 ,不要忘记给duration乘上time.Second
,否则发出来的token会当场过期
go
func CreateToken(userId int64, secret string, expire int64) (string, error) {
userClaims := UserClaims{
UserId: userId,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expire) * time.Second)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "app-api",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaims)
signed, err := token.SignedString([]byte(secret))
return signed, err
}
编写验证函数
ParseToken
要做的事情比较简单,只要尝试反序列化Token即可。这里的错误处理和一般情况有所不同,是因为token
有可能和err
一起返回(比如token过期),但是仍然可以尝试获取携带的信息。因此,如果token
不为nil
,则尝试解析余下信息。最后返回userClaims
和err
,如果err == nil
则认为解析成功
go
func ParseToken(tokenString string, secret string) (*UserClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret), nil
})
if token == nil {
return nil, err
}
userClaims, ok := token.Claims.(*UserClaims)
if !ok {
return nil, err
}
return userClaims, err
}
增加配置项
修改internal/config/config.go
,增加JwtAuth
项:
go
type Config struct {
rest.RestConf
UserRpc zrpc.RpcClientConf
JwtAuth struct {
Secret string
Expire int64
}
}
修改etc/app.yaml
,添加配置项:
yaml
JwtAuth:
Secret: ?xX7nYmfM<(4r%p
Expire: 1209600 # 14天
添加生成逻辑
调用CreateToken
很简单,如果遇到err
,就以ServerError
返回:
go
tokenString, err := utils.CreateToken(res.UserId, l.svcCtx.Config.JwtAuth.Secret, l.svcCtx.Config.JwtAuth.Expire)
if err != nil {
return nil, common.ServerError{
ApiError: common.ApiError{
StatusCode: 500,
Code: 50000,
Message: "Internal Server Error",
},
Detail: err,
}
}
最后只要把tokenString
放入请求响应就好。
测试注册函数
可以使用postman,apifox等调试工具发送请求,得到请求响应:
这样一来就完成了一个完整的注册函数。
完善登陆函数
这里仅贴出登录函数的实现,不作具体讲解:
go
func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
username := req.Username
password := req.Password
res, err := l.svcCtx.UserRpc.QueryByName(l.ctx, &user.QueryByNameRequest{
Username: username,
})
if err != nil {
if st, match := common.MatchError(err, userCommon.ErrUserNotFound); match {
return nil, common.ApiError{
StatusCode: 422,
Code: 42202,
Message: "用户名不存在",
}
} else {
for index, item := range st.Details() {
detail := item.(*anypb.Any)
fmt.Printf("%d: %s\n", index, string(detail.Value))
}
return nil, common.ServerError{
ApiError: common.ApiError{
StatusCode: 500,
Code: 50000,
Message: "Internal Server Error",
},
Detail: err,
}
}
}
err = bcrypt.CompareHashAndPassword(res.Password, []byte(password))
if err != nil {
return nil, common.ApiError{
StatusCode: 422,
Code: 42203,
Message: "密码错误",
}
}
tokenString, err := utils.CreateToken(res.UserId, l.svcCtx.Config.JwtAuth.Secret, l.svcCtx.Config.JwtAuth.Expire)
if err != nil {
return nil, common.ServerError{
ApiError: common.ApiError{
StatusCode: 500,
Code: 50000,
Message: "Internal Server Error",
},
Detail: err,
}
}
return &types.LoginResponse{
BasicResponse: types.BasicResponse{
StatusCode: 0,
StatusMsg: "",
},
UserId: res.UserId,
Token: tokenString,
}, nil
}
编写身份认证中间件
实际上,go-zero支持直接开启JWT令牌验证,并且会把解析到的键值放入ctx中,可以通过l.ctx.Value()
获取,详见go-zero.dev/docs/tutori...
但是,青训营使用的API并不是标准的JWT规范,它将token字段用query参数的方式发给后端。因此,我们只能自己编写中间件。
修改API定义
获取用户信息的接口需要JWT鉴权。go-zero支持分组绑定中间件,因此把之前定义的GetUserInfo接口单独放在一个service代码块中,并且加入middleware : JwtAuth
。冒号后面是给中间件起的名字
less
@server(
group : user
prefix: /douyin/user
middleware: JwtAuth
)
service app {
@handler GetUserInfo
get / (GetUserInfoRequest) returns (GetUserInfoResponse)
}
配置中间件
再次执行goctl api go --api app.api --dir=. --style=goZero
,可以看到新增internal/middleware/jwtauthMiddleware.go
。在开始编写逻辑之前还需要做一些准备工作。
配置serviceContext
在internal/svc/serviceContext.go
增加一项JwtAuth
字段,使用模板生成的NewJwtAuthMiddleware
产生中间件,传入config配置并获取其Handle
函数。
go
type ServiceContext struct {
Config config.Config
UserRpc userClient.User
JwtAuth rest.Middleware
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: userClient.NewUser(zrpc.MustNewClient(c.UserRpc)),
JwtAuth: middleware.NewJwtAuthMiddleware(c).Handle,
}
}
修改中间件配置
转到internal/middleware/jwtauthMiddleware.go
,需要接收传入的config配置并且将其添加到中间件的上下文:
go
type JwtAuthMiddleware struct {
Config config.Config
}
func NewJwtAuthMiddleware(c config.Config) *JwtAuthMiddleware {
return &JwtAuthMiddleware{
Config: c,
}
}
编写验证逻辑
中间件的逻辑是:
- 先尝试从URL获取query参数
token
,如果其不存在,则返回401错误 - 中间件尝试解析token。
- 如果返回err,并且err的原因是token过期,则返回友好提示;余下err返回token无效提示。
- 如果没有返回err,则说明token有效,不做处理
- 代码运行到最后,则放行请求
这里使用httpx.WriteJson
来提前返回响应。函数接收一个http.ResponseWriter
,HTTP状态码,以及一个interface
类型的返回值。
判断token解析错误类型时要使用errors.Is
,因为JWT这个库给错误外包装了一层错误,直接比较的话不会相等。想了解相关知识可以去搜索golang的错误链,Wrap以及Unwrap。
go
func (m *JwtAuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
httpx.WriteJson(w, http.StatusUnauthorized, &types.BasicResponse{
StatusCode: 40101,
StatusMsg: "没有提供token",
})
return
}
_, err := utils.ParseToken(token, m.Config.JwtAuth.Secret)
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
httpx.WriteJson(w, http.StatusUnauthorized, &types.BasicResponse{
StatusCode: 40102,
StatusMsg: "token已过期",
})
} else {
httpx.WriteJson(w, http.StatusUnauthorized, &types.BasicResponse{
StatusCode: 40103,
StatusMsg: "token无效",
})
}
return
}
next(w, r)
}
}
编写GetUserInfo函数
因为本文不会实现关注判断等业务逻辑,并且默认用户可以查看其他用户的信息,因此这个函数只需要调用一下RPC接口即可。其实这里的RPC错误处理重复了好几次有点繁琐,有改进空间。
go
func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoRequest) (resp *types.GetUserInfoResponse, err error) {
queryId := req.UserId
res, err := l.svcCtx.UserRpc.QueryById(l.ctx, &user.QueryByIdRequest{
UserId: queryId,
})
if err != nil {
if st, match := common.MatchError(err, userCommon.ErrUserNotFound); match {
return nil, common.ApiError{
StatusCode: 422,
Code: 42202,
Message: "用户名不存在",
}
} else {
for index, item := range st.Details() {
detail := item.(*anypb.Any)
fmt.Printf("%d: %s\n", index, string(detail.Value))
}
return nil, common.ServerError{
ApiError: common.ApiError{
StatusCode: 500,
Code: 50000,
Message: "Internal Server Error",
},
Detail: err,
}
}
}
return &types.GetUserInfoResponse{
BasicResponse: types.BasicResponse{
StatusCode: 200,
StatusMsg: "",
},
User: types.User{
Id: res.UserId,
Name: res.Username,
},
}, nil
}
查看中间件效果
依然是使用API调试软件,请求成功时将会正常返回信息:
查找不存在的用户会被
MatchError
函数捕获,并返回ApiError.Response()
:
token留空、过期会显示对应错误:
删改token使其失效,可以得到无效的提示:
结语
自此,我们已经完成了字段校验、RPC调用、错误处理、结果返回等任务,并搭建起一套用JWT实现用户登录、注册、获取信息的系统。
go-zero的http服务不如gin容易上手,因此配置serviceContext、中间件、路由的时候需要额外做一系列事情
我认为错误处理是编码过程中最大的难点,RPC status和error的类型转换、额外信息传递非常陌生,导致实现过程中代码的复用性比较差,判断逻辑也比较复杂,这方面的设计可以更加好些。