我们用一个系列来讲解从需求到上线、从代码到k8s部署、从日志到监控等各个方面的微服务完整实践。

整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中间件,所用到的技术栈基本是go-zero项目组的自研组件,基本是go-zero全家桶了。

实战项目地址:https://github.com/Mikaelemmmm/go-zero-looklook

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里面是什么

  1. package codes // import "google.golang.org/grpc/codes"
  2. import (
  3. "fmt"
  4. "strconv"
  5. )
  6. // A Code is an unsigned 32-bit error code as defined in the gRPC spec.
  7. type Code uint32
  8. .......

grpc的err对应的错误码其实就是一个uint32 , 我们自己定义错误用uint32然后在rpc的全局拦截器返回时候转成grpc的err,就可以了

所以我们自己定义全局错误码在app/common/xerr

errCode.go

  1. package xerr
  2. // 成功返回
  3. const OK uint32 = 200
  4. // 前3位代表业务,后三位代表具体功能
  5. // 全局错误码
  6. const SERVER_COMMON_ERROR uint32 = 100001
  7. const REUQES_PARAM_ERROR uint32 = 100002
  8. const TOKEN_EXPIRE_ERROR uint32 = 100003
  9. const TOKEN_GENERATE_ERROR uint32 = 100004
  10. const DB_ERROR uint32 = 100005
  11. // 用户模块

errMsg.go

  1. package xerr
  2. var message map[uint32]string
  3. func init() {
  4. message = make(map[uint32]string)
  5. message[OK] = "SUCCESS"
  6. message[SERVER_COMMON_ERROR] = "服务器开小差啦,稍后再来试一试"
  7. message[REUQES_PARAM_ERROR] = "参数错误"
  8. message[TOKEN_EXPIRE_ERROR] = "token失效,请重新登陆"
  9. message[TOKEN_GENERATE_ERROR] = "生成token失败"
  10. message[DB_ERROR] = "数据库繁忙,请稍后再试"
  11. }
  12. func MapErrMsg(errcode uint32) string {
  13. if msg, ok := message[errcode]; ok {
  14. return msg
  15. } else {
  16. return "服务器开小差啦,稍后再来试一试"
  17. }
  18. }
  19. func IsCodeErr(errcode uint32) bool {
  20. if _, ok := message[errcode]; ok {
  21. return true
  22. } else {
  23. return false
  24. }
  25. }

errors.go

  1. package xerr
  2. import "fmt"
  3. // 常用通用固定错误
  4. type CodeError struct {
  5. errCode uint32
  6. errMsg string
  7. }
  8. // 返回给前端的错误码
  9. func (e *CodeError) GetErrCode() uint32 {
  10. return e.errCode
  11. }
  12. // 返回给前端显示端错误信息
  13. func (e *CodeError) GetErrMsg() string {
  14. return e.errMsg
  15. }
  16. func (e *CodeError) Error() string {
  17. return fmt.Sprintf("ErrCode:%d,ErrMsg:%s", e.errCode, e.errMsg)
  18. }
  19. func NewErrCodeMsg(errCode uint32, errMsg string) *CodeError {
  20. return &CodeError{errCode: errCode, errMsg: errMsg}
  21. }
  22. func NewErrCode(errCode uint32) *CodeError {
  23. return &CodeError{errCode: errCode, errMsg: MapErrMsg(errCode)}
  24. }
  25. func NewErrMsg(errMsg string) *CodeError {
  26. return &CodeError{errCode: SERVER_COMMON_ERROR, errMsg: errMsg}
  27. }

比如我们在用户注册时候的rpc代码

  1. package logic
  2. import (
  3. "context"
  4. "looklook/app/identity/cmd/rpc/identity"
  5. "looklook/app/usercenter/cmd/rpc/internal/svc"
  6. "looklook/app/usercenter/cmd/rpc/usercenter"
  7. "looklook/app/usercenter/model"
  8. "looklook/common/xerr"
  9. "github.com/pkg/errors"
  10. "github.com/tal-tech/go-zero/core/logx"
  11. "github.com/tal-tech/go-zero/core/stores/sqlx"
  12. )
  13. var ErrUserAlreadyRegisterError = xerr.NewErrMsg("该用户已被注册")
  14. type RegisterLogic struct {
  15. ctx context.Context
  16. svcCtx *svc.ServiceContext
  17. logx.Logger
  18. }
  19. func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
  20. return &RegisterLogic{
  21. ctx: ctx,
  22. svcCtx: svcCtx,
  23. Logger: logx.WithContext(ctx),
  24. }
  25. }
  26. func (l *RegisterLogic) Register(in *usercenter.RegisterReq) (*usercenter.RegisterResp, error) {
  27. user, err := l.svcCtx.UserModel.FindOneByMobile(in.Mobile)
  28. if err != nil && err != model.ErrNotFound {
  29. return nil, errors.Wrapf(xerr.ErrDBError, "mobile:%s,err:%v", in.Mobile, err)
  30. }
  31. if user != nil {
  32. return nil, errors.Wrapf(ErrUserAlreadyRegisterError, "用户已经存在 mobile:%s,err:%v", in.Mobile, err)
  33. }
  34. var userId int64
  35. if err := l.svcCtx.UserModel.Trans(func(session sqlx.Session) error {
  36. user := new(model.User)
  37. user.Mobile = in.Mobile
  38. user.Nickname = in.Nickname
  39. insertResult, err := l.svcCtx.UserModel.Insert(session, user)
  40. if err != nil {
  41. return errors.Wrapf(xerr.ErrDBError, "err:%v,user:%+v", err, user)
  42. }
  43. lastId, err := insertResult.LastInsertId()
  44. if err != nil {
  45. return errors.Wrapf(xerr.ErrDBError, "insertResult.LastInsertId err:%v,user:%+v", err, user)
  46. }
  47. userId = lastId
  48. userAuth := new(model.UserAuth)
  49. userAuth.UserId = lastId
  50. userAuth.AuthKey = in.AuthKey
  51. userAuth.AuthType = in.AuthType
  52. if _, err := l.svcCtx.UserAuthModel.Insert(session, userAuth); err != nil {
  53. return errors.Wrapf(xerr.ErrDBError, "err:%v,userAuth:%v", err, userAuth)
  54. }
  55. return nil
  56. }); err != nil {
  57. return nil, err
  58. }
  59. // 2、生成token.
  60. resp, err := l.svcCtx.IdentityRpc.GenerateToken(l.ctx, &identity.GenerateTokenReq{
  61. UserId: userId,
  62. })
  63. if err != nil {
  64. return nil, errors.Wrapf(ErrGenerateTokenError, "IdentityRpc.GenerateToken userId : %d , err:%+v", userId, err)
  65. }
  66. return &usercenter.RegisterResp{
  67. AccessToken: resp.AccessToken,
  68. AccessExpire: resp.AccessExpire,
  69. RefreshAfter: resp.RefreshAfter,
  70. }, nil
  71. }
  1. 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的全局拦截器,这个很重要 ,如果不加这个没办法实现

  1. package main
  2. ......
  3. func main() {
  4. ........
  5. //rpc log,grpc的全局拦截器
  6. s.AddUnaryInterceptors(rpcserver.LoggerInterceptor)
  7. .......
  8. }

我们看看rpcserver.LoggerInterceptor的具体实现

  1. import (
  2. ...
  3. "github.com/pkg/errors"
  4. )
  5. func LoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
  6. resp, err = handler(ctx, req)
  7. if err != nil {
  8. causeErr := errors.Cause(err) // err类型
  9. if e, ok := causeErr.(*xerr.CodeError); ok { //自定义错误类型
  10. logx.WithContext(ctx).Errorf("【RPC-SRV-ERR】 %+v", err)
  11. //转成grpc err
  12. err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
  13. } else {
  14. logx.WithContext(ctx).Errorf("【RPC-SRV-ERR】 %+v", err)
  15. }
  16. }
  17. return resp, err
  18. }

当有请求进入到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步的错误信息 代码如下

  1. ......
  2. func (l *RegisterLogic) Register(req types.RegisterReq) (*types.RegisterResp, error) {
  3. registerResp, err := l.svcCtx.UsercenterRpc.Register(l.ctx, &usercenter.RegisterReq{
  4. Mobile: req.Mobile,
  5. Nickname: req.Nickname,
  6. AuthKey: req.Mobile,
  7. AuthType: model.UserAuthTypeSystem,
  8. })
  9. if err != nil {
  10. return nil, errors.Wrapf(err, "req: %+v", req)
  11. }
  12. var resp types.RegisterResp
  13. _ = copier.Copy(&resp, registerResp)
  14. return &resp, nil
  15. }

这里同样是使用标准包的errors.Wrapf , 也就是说所有我们业务中返回错误都适用标准包的errors,但是内部参数要使用我们xerr定义的错误

这里有2个注意点

1)api服务想把rpc返回给前端友好的错误提示信息,我们想直接返回给前端不做任何处理(比如rpc已经返回了“用户已存在”,api不想做什么处理,就想把这个错误信息直接返回给前端)

针对这种情况,直接就像上图这种写就可以了,将rpc调用处的err直接作为errors.Wrapf 第一个参数扔出去,但是第二个参数最好记录一下自己需要的详细日志方便后续在api log里查看

2)api服务不管rpc返回的是什么错误信息,我就想自己再重新定义给前端返回错误信息(比如rpc已经返回了“用户已存在”,api想调用rpc时只要有错误我就返回给前端“用户注册失败”)

针对这种情况,如下这样写即可(当然你可以将xerr.NewErrMsg("用户注册失败") 放到代码上方使用一个变量,这里放变量也可以)

  1. func (l *RegisterLogic) Register(req types.RegisterReq) (*types.RegisterResp, error) {
  2. .......
  3. if err != nil {
  4. return nil, errors.Wrapf(xerr.NewErrMsg("用户注册失败"), "req: %+v,rpc err:%+v", req,err)
  5. }
  6. .....
  7. }

接下来我们看最终返回给前端怎么处理的,我们接着看app/usercenter/cmd/api/internal/handler/user/registerHandler.go

  1. func RegisterHandler(ctx *svc.ServiceContext) http.HandlerFunc {
  2. return func(w http.ResponseWriter, r *http.Request) {
  3. var req types.RegisterReq
  4. if err := httpx.Parse(r, &req); err != nil {
  5. httpx.Error(w, err)
  6. return
  7. }
  8. l := user.NewRegisterLogic(r.Context(), ctx)
  9. resp, err := l.Register(req)
  10. result.HttpResult(r, w, resp, err)
  11. }
  12. }

这里可以看到,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很简单,专门处理参数错误的

  1. // http 参数错误返回
  2. func ParamErrorResult(r *http.Request, w http.ResponseWriter, err error) {
  3. errMsg := fmt.Sprintf("%s ,%s", xerr.MapErrMsg(xerr.REUQES_PARAM_ERROR), err.Error())
  4. httpx.WriteJson(w, http.StatusBadRequest, Error(xerr.REUQES_PARAM_ERROR, errMsg))
  5. }

我们主要来看HttpResult , 业务返回的错误处理的

  1. // http返回
  2. func HttpResult(r *http.Request, w http.ResponseWriter, resp interface{}, err error) {
  3. if err == nil {
  4. // 成功返回
  5. r := Success(resp)
  6. httpx.WriteJson(w, http.StatusOK, r)
  7. } else {
  8. // 错误返回
  9. errcode := xerr.SERVER_COMMON_ERROR
  10. errmsg := "服务器开小差啦,稍后再来试一试"
  11. causeErr := errors.Cause(err) // err类型
  12. if e, ok := causeErr.(*xerr.CodeError); ok {
  13. // 自定义错误类型
  14. // 自定义CodeError
  15. errcode = e.GetErrCode()
  16. errmsg = e.GetErrMsg()
  17. } else {
  18. if gstatus, ok := status.FromError(causeErr); ok {
  19. // grpc err错误
  20. grpcCode := uint32(gstatus.Code())
  21. if xerr.IsCodeErr(grpcCode) {
  22. // 区分自定义错误跟系统底层、db等错误,底层、db错误不能返回给前端
  23. errcode = grpcCode
  24. errmsg = gstatus.Message()
  25. }
  26. }
  27. }
  28. logx.WithContext(r.Context()).Errorf("【API-ERR】 : %+v ", err)
  29. httpx.WriteJson(w, http.StatusBadRequest, Error(errcode, errmsg))
  30. }
  31. }

err : 要记录的日志错误

errcode : 返回给前端的错误码

errmsg :返回给前端的友好的错误提示信息

成功直接返回,如果遇到错误了,也是使用github.com/pkg/errors这个包来判断错误,是不是我们自己定义的错误(api中定义的错误直接使用我们自己定义的xerr),还是grpc错误(rpc业务抛出来的),如果是grpc错误在通过uint32转成我们自己错误码,根据错误码再去我们自己定义错误信息中找到定义的错误信息返回给前端,如果是api错误直接返回给前端我们自己定义的错误信息,都找不到那就返回默认错误“服务器开小差了” ,

4、结尾

到这里错误处理已经消息描述清楚了,接下来我们要看打印了服务端的错误日志,我们该如何收集查看,就涉及到日志收集系统。

项目地址

https://github.com/zeromicro/go-zero

欢迎使用 go-zerostar 支持我们!

微信交流群

关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

微服务从代码到k8s部署应有尽有系列(十、错误处理)的更多相关文章

  1. 微服务从代码到k8s部署应有尽有系列(一)

    从本篇文章开始,我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 实战项目地址:https://github.com/Mikaelemmmm/go-zer ...

  2. 微服务从代码到k8s部署应有尽有系列(二、网关)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  3. 微服务从代码到k8s部署应有尽有系列(三、鉴权)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  4. 微服务从代码到k8s部署应有尽有系列(四、用户中心)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  5. 微服务从代码到k8s部署应有尽有系列(五、民宿服务)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  6. 微服务从代码到k8s部署应有尽有系列(六、订单服务)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  7. 微服务从代码到k8s部署应有尽有系列(十四、部署环境搭建)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  8. 微服务从代码到k8s部署应有尽有系列(七、支付服务)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  9. 微服务从代码到k8s部署应有尽有系列(八、各种队列)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

  10. 微服务从代码到k8s部署应有尽有系列(九、事务精讲)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...

随机推荐

  1. 关于new Date总结及注意事项

    记录关于 new Date() 的一些常用方法及问题 new Date()基本方法: 创建一个日期对象的几种方法 注意: 由于浏览器差异和不一致性,强烈建议不要使用Date构造函数(和Date.par ...

  2. 从零开始, 开发一个 Web Office 套件 (2): 富文本编辑器

    书接前文: 从零开始, 开发一个 Web Office 套件 (1): 富文本编辑器 这是一个系列博客, 最终目的是要做一个基于HTML Canvas 的, 类似于微软 Office 的 Web Of ...

  3. 一劳永逸,解决.NET发布云服务器的时区问题

    国内大多数开发者使用的电脑,都是使用的北京时间,日常开发的过程中其实并没有什么不便:不过,等遇到了阿里云等云服务器,系统默认使用的时间大多为UTC时间,这个时候,时区和时间的问题,就是不容忽视的大问题 ...

  4. 深入学习python内存管理

    深入Python的内存管理   作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 语言的内存管理是语言设计的一个重要方面.它是决定语 ...

  5. freeswitch对接电信线路VOLTE视频通话

    在public.xml上设置视频编码: <action application="export" data="nolocal:absolute_codec_stri ...

  6. HTML加载动画实现

    在页面加载完成之前或者执行某操作时,先显示要显示的加载动画. 实现原理 先定义一个加载动画元素(最好是纯CSS实现,图片的话可能慢一点),当页面未加载完成之前,先使其"可见",当页 ...

  7. RabbitMQ如何实现延迟队列?(转)

    什么是延迟队列 延迟队列存储的对象肯定是对应的延迟消息,所谓"延迟消息"是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费. 场景一 ...

  8. android怎么做表格显示数据

    实现思路:最底层(父级)背景为黑色,最上层(子级)背景为白色,然后父子组件之间存在一丝间隔即可显示出类似边框的线. 本次主要利用Android中的TableRow等实现,其他类比也可以实现效果. &l ...

  9. Docker创建私有镜像仓库

    Docker官方提供了一个工具docker-registry,可以借助这个工具构建私有镜像仓库: 1.拉取registry镜像 # docker pull registry//可以使用 docker ...

  10. 基于6U VPX TMS320C6678+XC7K325T 的信号处理板

    一.板卡概述 本板卡基于6U VPX结构设计无线电信号处理平台.板卡包含1片C6678芯片,1片 FPGA XC7K325T-2FFG900I:4路AD,4路DA:三个双排插针接口,要求承接前端射频电 ...