golang-im聊天
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

错误处理.md 4.7 KiB

2 jaren geleden
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. ### 错误处理,链路追踪,日志打印
  2. 系统中的错误一般可以归类为两种,一种是业务定义的错误,一种就是未知的错误,在业务正式上线的时候,业务定义的错误的属于正常业务逻辑,不需要打印出来,
  3. 但是未知的错误,我们就需要打印出来,我们不仅要知道是什么错误,还要知道错误的调用堆栈,所以这里我对GRPC的错误进行了一些封装,使之包含调用堆栈。
  4. ```go
  5. func WrapError(err error) error {
  6. if err == nil {
  7. return nil
  8. }
  9. s := &spb.Status{
  10. Code: int32(codes.Unknown),
  11. Message: err.Error(),
  12. Details: []*any.Any{
  13. {
  14. TypeUrl: TypeUrlStack,
  15. Value: util.Str2bytes(stack()),
  16. },
  17. },
  18. }
  19. return status.FromProto(s).Err()
  20. }
  21. // Stack 获取堆栈信息
  22. func stack() string {
  23. var pc = make([]uintptr, 20)
  24. n := runtime.Callers(3, pc)
  25. var build strings.Builder
  26. for i := 0; i < n; i++ {
  27. f := runtime.FuncForPC(pc[i] - 1)
  28. file, line := f.FileLine(pc[i] - 1)
  29. n := strings.Index(file, name)
  30. if n != -1 {
  31. s := fmt.Sprintf(" %s:%d \n", file[n:], line)
  32. build.WriteString(s)
  33. }
  34. }
  35. return build.String()
  36. }
  37. ```
  38. 这样,不仅可以拿到错误的堆栈,错误的堆栈也可以跨RPC传输,但是,但是这样你只能拿到当前服务的堆栈,却不能拿到调用方的堆栈,就比如说,A服务调用
  39. B服务,当B服务发生错误时,在A服务通过日志打印错误的时候,我们只打印了B服务的调用堆栈,怎样可以把A服务的堆栈打印出来。我们在A服务调用的地方也获取
  40. 一次堆栈。
  41. ```go
  42. func WrapRPCError(err error) error {
  43. if err == nil {
  44. return nil
  45. }
  46. e, _ := status.FromError(err)
  47. s := &spb.Status{
  48. Code: int32(e.Code()),
  49. Message: e.Message(),
  50. Details: []*any.Any{
  51. {
  52. TypeUrl: TypeUrlStack,
  53. Value: util.Str2bytes(GetErrorStack(e) + " --grpc-- \n" + stack()),
  54. },
  55. },
  56. }
  57. return status.FromProto(s).Err()
  58. }
  59. func interceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
  60. err := invoker(ctx, method, req, reply, cc, opts...)
  61. return gerrors.WrapRPCError(err)
  62. }
  63. var LogicIntClient pb.LogicIntClient
  64. func InitLogicIntClient(addr string) {
  65. conn, err := grpc.DialContext(context.TODO(), addr, grpc.WithInsecure(), grpc.WithUnaryInterceptor(interceptor))
  66. if err != nil {
  67. logger.Sugar.Error(err)
  68. panic(err)
  69. }
  70. LogicIntClient = pb.NewLogicIntClient(conn)
  71. }
  72. ```
  73. 像这样,就可以获取完整一次调用堆栈。
  74. 错误打印也没有必要在函数返回错误的时候,每次都去打印。因为错误已经包含了堆栈信息
  75. ```go
  76. // 错误的方式
  77. if err != nil {
  78. logger.Sugar.Error(err)
  79. return err
  80. }
  81. // 正确的方式
  82. if err != nil {
  83. return err
  84. }
  85. ```
  86. 然后,我们在上层统一打印就可以
  87. ```go
  88. func startServer {
  89. extListen, err := net.Listen("tcp", conf.LogicConf.ClientRPCExtListenAddr)
  90. if err != nil {
  91. panic(err)
  92. }
  93. extServer := grpc.NewServer(grpc.UnaryInterceptor(LogicClientExtInterceptor))
  94. pb.RegisterLogicClientExtServer(extServer, &LogicClientExtServer{})
  95. err = extServer.Serve(extListen)
  96. if err != nil {
  97. panic(err)
  98. }
  99. }
  100. func LogicClientExtInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
  101. defer logPanic("logic_client_ext_interceptor", ctx, req, info, &err)
  102. resp, err = handler(ctx, req)
  103. logger.Logger.Debug("logic_client_ext_interceptor", zap.Any("info", info), zap.Any("ctx", ctx), zap.Any("req", req),
  104. zap.Any("resp", resp), zap.Error(err))
  105. s, _ := status.FromError(err)
  106. if s.Code() != 0 && s.Code() < 1000 {
  107. md, _ := metadata.FromIncomingContext(ctx)
  108. logger.Logger.Error("logic_client_ext_interceptor", zap.String("method", info.FullMethod), zap.Any("md", md), zap.Any("req", req),
  109. zap.Any("resp", resp), zap.Error(err), zap.String("stack", gerrors.GetErrorStack(s)))
  110. }
  111. return
  112. }
  113. ```
  114. 这样做的前提就是,在业务代码中透传context,golang不像其他语言,可以在线程本地保存变量,像Java的ThreadLocal,所以只能通过函数参数的形式进行传递,im中,service层函数的第一个参数
  115. 都是context,但是dao层和cache层就不需要了,不然,显得代码臃肿。
  116. 最后可以在客户端的每次请求添加一个随机的request_id,这样客户端到服务的每次请求都可以串起来了。
  117. ```go
  118. func getCtx() context.Context {
  119. token, _ := util.GetToken(1, 2, 3, time.Now().Add(1*time.Hour).Unix(), util.PublicKey)
  120. return metadata.NewOutgoingContext(context.TODO(), metadata.Pairs(
  121. "app_id", "1",
  122. "user_id", "2",
  123. "device_id", "3",
  124. "token", token,
  125. "request_id", strconv.FormatInt(time.Now().UnixNano(), 10)))
  126. }
  127. ```