咱们用一个系列来解说从需要到上线、从代码到 k8s 部署、从日志到监控等各个方面的微服务残缺实际。
整个我的项目应用了 go-zero 开发的微服务,根本蕴含了 go-zero 以及相干 go-zero 作者开发的一些中间件,所用到的技术栈根本是 go-zero 项目组的自研组件,根本是 go-zero 全家桶了。
实战我的项目地址:https://github.com/Mikaelemmm…
1、概述
咱们在平时开发时候,程序在出错时,心愿能够通过谬误日志能疾速定位问题(那么传递进来的参数、包含堆栈信息必定就要都要打印到日志),但同时又想返回给前端用户比拟友善、能看得懂的谬误提醒,那这两点如果只通过一个 fmt.Error、errors.new 等返回一个错误信息必定是无奈做到的,除非在返回前端谬误提醒的中央同时在记录 log,这样的话日志满天飞,代码难看不说,日志到时候也会很难看。
那么咱们想一下,如果有一个对立的中央记录日志,同时在业务代码中只须要一个 return err 就能将返回给前端的谬误提示信息、日志记录置信信息离开提醒跟记录,如果依照这个思路实现,那几乎不要太爽,是的 go-zero-looklook 就是这么解决的,接下来咱们看下。
2、rpc 错误处理
依照失常状况下,go-zero 的 rpc 服务是基于 grpc 的,默认返回的谬误是 grpc 的 status.Error 没法给咱们自定义的谬误合并,并且也不适宜咱们自定义的谬误,它的错误码、谬误类型都是定义死在 grpc 包中的,ok,如果咱们在 rpc 中能用自定义谬误返回,而后在拦截器对立返回时候转成 grpc 的 status.Error,那么咱们 rpc 的 err 跟 api 的 err 是不是能够对立治理咱们本人的谬误了呢?
咱们看一下 grpc 的 status.Error 的 code 外面是什么
package codes // import "google.golang.org/grpc/codes"
import (
"fmt"
"strconv"
)
// A Code is an unsigned 32-bit error code as defined in the gRPC spec.
type Code uint32
.......
grpc 的 err 对应的错误码其实就是一个 uint32,咱们本人定义谬误用 uint32 而后在 rpc 的全局拦截器返回时候转成 grpc 的 err,就能够了
所以咱们本人定义全局错误码在 app/common/xerr
errCode.go
package xerr
// 胜利返回
const OK uint32 = 200
// 前 3 位代表业务, 后三位代表具体性能
// 全局错误码
const SERVER_COMMON_ERROR uint32 = 100001
const REUQES_PARAM_ERROR uint32 = 100002
const TOKEN_EXPIRE_ERROR uint32 = 100003
const TOKEN_GENERATE_ERROR uint32 = 100004
const DB_ERROR uint32 = 100005
// 用户模块
errMsg.go
package xerr
var message map[uint32]string
func init() {message = make(map[uint32]string)
message[OK] = "SUCCESS"
message[SERVER_COMMON_ERROR] = "服务器开小差啦, 稍后再来试一试"
message[REUQES_PARAM_ERROR] = "参数谬误"
message[TOKEN_EXPIRE_ERROR] = "token 生效,请从新登陆"
message[TOKEN_GENERATE_ERROR] = "生成 token 失败"
message[DB_ERROR] = "数据库忙碌, 请稍后再试"
}
func MapErrMsg(errcode uint32) string {if msg, ok := message[errcode]; ok {return msg} else {return "服务器开小差啦, 稍后再来试一试"}
}
func IsCodeErr(errcode uint32) bool {if _, ok := message[errcode]; ok {return true} else {return false}
}
errors.go
package xerr
import "fmt"
// 罕用通用固定谬误
type CodeError struct {
errCode uint32
errMsg string
}
// 返回给前端的错误码
func (e *CodeError) GetErrCode() uint32 {return e.errCode}
// 返回给前端显示端错误信息
func (e *CodeError) GetErrMsg() string {return e.errMsg}
func (e *CodeError) Error() string {return fmt.Sprintf("ErrCode:%d,ErrMsg:%s", e.errCode, e.errMsg)
}
func NewErrCodeMsg(errCode uint32, errMsg string) *CodeError {return &CodeError{errCode: errCode, errMsg: errMsg}
}
func NewErrCode(errCode uint32) *CodeError {return &CodeError{errCode: errCode, errMsg: MapErrMsg(errCode)}
}
func NewErrMsg(errMsg string) *CodeError {return &CodeError{errCode: SERVER_COMMON_ERROR, errMsg: errMsg}
}
比方咱们在用户注册时候的 rpc 代码
package logic
import (
"context"
"looklook/app/identity/cmd/rpc/identity"
"looklook/app/usercenter/cmd/rpc/internal/svc"
"looklook/app/usercenter/cmd/rpc/usercenter"
"looklook/app/usercenter/model"
"looklook/common/xerr"
"github.com/pkg/errors"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/stores/sqlx"
)
var ErrUserAlreadyRegisterError = xerr.NewErrMsg("该用户已被注册")
type RegisterLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *RegisterLogic) Register(in *usercenter.RegisterReq) (*usercenter.RegisterResp, error) {user, err := l.svcCtx.UserModel.FindOneByMobile(in.Mobile)
if err != nil && err != model.ErrNotFound {return nil, errors.Wrapf(xerr.ErrDBError, "mobile:%s,err:%v", in.Mobile, err)
}
if user != nil {return nil, errors.Wrapf(ErrUserAlreadyRegisterError, "用户曾经存在 mobile:%s,err:%v", in.Mobile, err)
}
var userId int64
if err := l.svcCtx.UserModel.Trans(func(session sqlx.Session) error {user := new(model.User)
user.Mobile = in.Mobile
user.Nickname = in.Nickname
insertResult, err := l.svcCtx.UserModel.Insert(session, user)
if err != nil {return errors.Wrapf(xerr.ErrDBError, "err:%v,user:%+v", err, user)
}
lastId, err := insertResult.LastInsertId()
if err != nil {return errors.Wrapf(xerr.ErrDBError, "insertResult.LastInsertId err:%v,user:%+v", err, user)
}
userId = lastId
userAuth := new(model.UserAuth)
userAuth.UserId = lastId
userAuth.AuthKey = in.AuthKey
userAuth.AuthType = in.AuthType
if _, err := l.svcCtx.UserAuthModel.Insert(session, userAuth); err != nil {return errors.Wrapf(xerr.ErrDBError, "err:%v,userAuth:%v", err, userAuth)
}
return nil
}); err != nil {return nil, err}
// 2、生成 token.
resp, err := l.svcCtx.IdentityRpc.GenerateToken(l.ctx, &identity.GenerateTokenReq{UserId: userId,})
if err != nil {return nil, errors.Wrapf(ErrGenerateTokenError, "IdentityRpc.GenerateToken userId : %d , err:%+v", userId, err)
}
return &usercenter.RegisterResp{
AccessToken: resp.AccessToken,
AccessExpire: resp.AccessExpire,
RefreshAfter: resp.RefreshAfter,
}, nil
}
errors.Wrapf(ErrUserAlreadyRegisterError, "用户曾经存在 mobile:%s,err:%v", in.Mobile, err)
这里咱们应用 go 默认的 errors 的包的 errors.Wrapf(如果这里不明确就去查一下 go 的 errors 包下的 Wrap、Wrapf 等)
第一个参数,ErrUserAlreadyRegisterError 定义在上方 就是应用 xerr.NewErrMsg(“ 该用户已被注册 ”),返回给前端敌对的提醒,要记住这里用的是咱们 xerr 包下的办法
第二个参数,就是记录在服务器日志,能够写具体一点都没关系只会记录在服务器不会被返回给前端
那咱们来看看为什么第一个参数就能是返回给前端的,第二个参数就是记录日志的
⚠️【注】咱们在 rpc 的启动文件 main 办法中,加了 grpc 的全局拦截器,这个很重要,如果不加这个没方法实现
package main
......
func main() {
........
//rpc log,grpc 的全局拦截器
s.AddUnaryInterceptors(rpcserver.LoggerInterceptor)
.......
}
咱们看看 rpcserver.LoggerInterceptor 的具体实现
import (
...
"github.com/pkg/errors"
)
func LoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {resp, err = handler(ctx, req)
if err != nil {causeErr := errors.Cause(err) // err 类型
if e, ok := causeErr.(*xerr.CodeError); ok { // 自定义谬误类型
logx.WithContext(ctx).Errorf("【RPC-SRV-ERR】%+v", err)
// 转成 grpc err
err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
} else {logx.WithContext(ctx).Errorf("【RPC-SRV-ERR】%+v", err)
}
}
return resp, err
}
当有申请进入到 rpc 服务时候,先进入拦截器而后就是执行 handler 办法,如果你想在进入之前解决某些事件就能够写在 handler 办法之前,那咱们想解决的是返回后果如果有谬误的状况,所以咱们在 handler 下方应用了 github.com/pkg/errors 这个包,这个包处理错误是 go 中常常用到的这不是官网的 errors 包,然而设计的很好,go 官网的 Wrap、Wrapf 等就是借鉴了这个包的思路。
因为咱们 grpc 外部业务在返回谬误时候
1)如果是咱们本人业务谬误,咱们会对立用 xerr 生成谬误,这样就能够拿到咱们定义的错误信息,因为后面咱们本人谬误也是用的 uint32,所以在这里对立转成 grpc 谬误 err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg()),那这里获取到的,e.GetErrCode()就是咱们定义的 code,e.GetErrMsg() 就是咱们之前定义返回的谬误第二个参数
2)然而还有一种状况是 rpc 服务异样了底部抛出来的谬误,自身就是 grpc 谬误了,那这种的咱们间接就记录异样就好了
3、api 谬误
当咱们 api 在 logic 中调用 rpc 的 Register 时候,rpc 返回了下面第 2 步的错误信息 代码如下
......
func (l *RegisterLogic) Register(req types.RegisterReq) (*types.RegisterResp, error) {
registerResp, err := l.svcCtx.UsercenterRpc.Register(l.ctx, &usercenter.RegisterReq{
Mobile: req.Mobile,
Nickname: req.Nickname,
AuthKey: req.Mobile,
AuthType: model.UserAuthTypeSystem,
})
if err != nil {return nil, errors.Wrapf(err, "req: %+v", req)
}
var resp types.RegisterResp
_ = copier.Copy(&resp, registerResp)
return &resp, nil
}
这里同样是应用规范包的 errors.Wrapf,也就是说所有咱们业务中返回谬误都实用规范包的 errors,然而外部参数要应用咱们 xerr 定义的谬误
这里有 2 个留神点
1)api 服务想把 rpc 返回给前端敌对的谬误提示信息,咱们想间接返回给前端不做任何解决(比方 rpc 曾经返回了“用户已存在”,api 不想做什么解决,就想把这个错误信息间接返回给前端)
针对这种状况,间接就像上图这种写就能够了,将 rpc 调用处的 err 间接作为 errors.Wrapf 第一个参数扔出去,然而第二个参数最好记录一下本人须要的具体日志不便后续在 api log 里查看
2)api 服务不论 rpc 返回的是什么错误信息,我就想本人再从新定义给前端返回错误信息(比方 rpc 曾经返回了“用户已存在”,api 想调用 rpc 时只有有谬误我就返回给前端“用户注册失败”)
针对这种状况,如下这样写即可(当然你能够将 xerr.NewErrMsg(“ 用户注册失败 ”) 放到代码上方应用一个变量,这里放变量也能够)
func (l *RegisterLogic) Register(req types.RegisterReq) (*types.RegisterResp, error) {
.......
if err != nil {return nil, errors.Wrapf(xerr.NewErrMsg("用户注册失败"), "req: %+v,rpc err:%+v", req,err)
}
.....
}
接下来咱们看最终返回给前端怎么解决的,咱们接着看 app/usercenter/cmd/api/internal/handler/user/registerHandler.go
func RegisterHandler(ctx *svc.ServiceContext) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) {
var req types.RegisterReq
if err := httpx.Parse(r, &req); err != nil {httpx.Error(w, err)
return
}
l := user.NewRegisterLogic(r.Context(), ctx)
resp, err := l.Register(req)
result.HttpResult(r, w, resp, err)
}
}
这里能够看到,go-zero-looklook 生成的 handler 代码 有 2 个中央跟默认官网的 goctl 生成的代码不一样,就是在处理错误解决的时候,这里替换成咱们本人的错误处理了,在 common/result/httpResult.go
【注】有人会说,每次应用 goctl 都要过去手动改,那不是要麻烦死了,这里咱们应用 go-zero 给咱们提供的 template 模版性能(还不晓得这个的就要去官网文档学习一下了),批改一下 handler 生成模版即可,整个我的项目的模版文件放在 deploy/goctl 下,这里 hanlder 批改的模版在 deploy/goctl/1.2.3-cli/api/handler.tpl
ParamErrorResult 很简略,专门解决参数谬误的
// http 参数谬误返回
func ParamErrorResult(r *http.Request, w http.ResponseWriter, err error) {errMsg := fmt.Sprintf("%s ,%s", xerr.MapErrMsg(xerr.REUQES_PARAM_ERROR), err.Error())
httpx.WriteJson(w, http.StatusBadRequest, Error(xerr.REUQES_PARAM_ERROR, errMsg))
}
咱们次要来看 HttpResult,业务返回的错误处理的
// 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))
}
}
err:要记录的日志谬误
errcode:返回给前端的错误码
errmsg:返回给前端的敌对的谬误提示信息
胜利间接返回,如果遇到谬误了,也是应用 github.com/pkg/errors 这个包来判断谬误,是不是咱们本人定义的谬误(api 中定义的谬误间接应用咱们本人定义的 xerr),还是 grpc 谬误(rpc 业务抛出来的),如果是 grpc 谬误在通过 uint32 转成咱们本人错误码,依据错误码再去咱们本人定义错误信息中找到定义的错误信息返回给前端,如果是 api 谬误间接返回给前端咱们本人定义的错误信息,都找不到那就返回默认谬误“服务器开小差了”,
4、结尾
到这里错误处理曾经音讯形容分明了,接下来咱们要看打印了服务端的谬误日志,咱们该如何收集查看,就波及到日志收集零碎。
我的项目地址
https://github.com/zeromicro/go-zero
欢送应用 go-zero
并 star 反对咱们!
微信交换群
关注『微服务实际 』公众号并点击 交换群 获取社区群二维码。