热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

GOGRPC实践(二)增加拦截器,实现自定义context(带request_id)、recover以及请求日志打印

demo代码地址https:github.comMe1onRindgo-demo拦截器原理和gin或django的middleware一样,在请求真正到达请求方法之前,框架会依次调

demo代码地址

https://github.com/Me1onRind/go-demo


拦截器原理

和gin或django的middleware一样, 在请求真正到达请求方法之前, 框架会依次调用注册的middleware函数, 可以基于此方便的对每个请求进行身份验证、日志记录、限流等功能


拦截器函数原型

func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

入参



  • ctx 请求上下文

  • req 请求报文

  • info 请求的接口信息

  • handler 下一个拦截器(或真正的请求方法)


返回值



  • resp 返回报文

  • err 错误


新增目录

├── internal
  ├── core
      ├── common
      │   ├── context.go # 自定义上下文
      ├── middleware
         ├── context.go # 生成自定义上下文
         ├── logger.go # 日志记录
        └── recover.go # recover

代码实现


自定义上下文

​ go语言中自身没有支持类似于java的 LocalThread变量, 也不推荐使用(如用协程id+map), 而是推荐使用一个上下文变量显示的传递。 而在实际使用(如记录请求的request_id)时, go语言自带的context.Context并不能很好的满足需求(取值时需要断言, 不方便维护也容易出问题)。

实践中一个比较好的办法就是实现一个自定义的context


common/context.go

zap.Logger的用法不是重点, 这里只是简单的初始化

package common
import (
"context"
"os"
"github.com/google/uuid"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type contextKey struct{}
var (
logger *zap.Logger
cKey = contextKey{}
)
func init() {
config := zap.NewProductionEncoderConfig()
config.EncodeDuration = zapcore.MillisDurationEncoder
config.EncodeTime = zapcore.ISO8601TimeEncoder
core := zapcore.NewCore(zapcore.NewConsoleEncoder(config), zapcore.AddSync(os.Stdout), zapcore.InfoLevel)
logger = zap.New(core, zap.AddCaller())
}
type Context struct {
context.Context
Logger *zap.Logger // 带上下文信息的logger, 如request_id
}
func NewContext(ctx context.Context) *Context {
c := &Context{}
c.COntext= storeContext(ctx, c)
requestID, _ := uuid.NewRandom()
c.Logger = logger.With(zap.String("request_id", requestID.String()))
return c
}
// 拦截器之间直接只能通过context.Context传递, 所以需要将自定义context存到go的context里向下传
func storeContext(c context.Context, ctx *Context) context.Context {
return context.WithValue(c, cKey, ctx)
}
func GetContext(c context.Context) *Context {
return c.Value(cKey).(*Context)
}

拦截器


middleware/context.go

生成自定义context

package middleware
import (
"context"
"github.com/Me1onRind/go-demo/internal/core/common"
"google.golang.org/grpc"
)
func GrpcContext() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
commonCtx := common.NewContext(ctx)
return handler(commonCtx, req)
}
}

middleware/recover.go

recover防止单个请求中的panic, 导致整个进程挂掉, 同时将panic时的堆栈信息保存到日志文件, 以及返回error信息

package middleware
import (
"context"
"errors"
"fmt"
"runtime/debug"
"github.com/Me1onRind/go-demo/internal/core/common"
"go.uber.org/zap"
"google.golang.org/grpc"
)
func GrpcRecover() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
defer func() {
commonCtx := common.GetContext(ctx)
if e := recover(); e != nil {
commonCtx.Logger.Error("server panic", zap.Any("panicErr", e))
commonCtx.Logger.Sugar().Errorf("%s", debug.Stack())
err = errors.New(fmt.Sprintf("panic:%v", e))
}
}()
resp, err = handler(ctx, req)
return resp, err
}
}

middleware/logger.go

记录请求的入参、返回值、请求方法和耗时

使用defer而不是放在handler之后是 防止打印日志之前代码panic, 类似的场景都可以使用defer来保证函数退出时某些步骤必须执行

package middleware
import (
"context"
"time"
"github.com/Me1onRind/go-demo/internal/core/common"
"go.uber.org/zap"
"google.golang.org/grpc"
)
func GrpcLogger() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
begin := time.Now()
defer func() {
commonCtx := common.GetContext(ctx)
commonCtx.Logger.Info("access request", zap.Reflect("req", req), zap.Reflect("resp", resp),
zap.String("method", info.FullMethod), zap.Error(err), zap.Duration("cost", time.Since(begin)),
)
}()
resp, err = handler(ctx, req)
return resp, err
}
}

将拦截器加载到grpc.Server中

原生的grpc.Server只支持加载一个拦截器, 为了避免将所有拦截器功能写到一个函数里 使用go-grpc-middleware这个第三方包, 相当于提供一个使用多个拦截器的语法糖

拦截器执行顺序和入参顺序保持一致

package main
import (
// ...
"github.com/Me1onRind/go-demo/internal/core/middleware"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
)
func main() {
// ...
s := grpc.NewServer(grpc_middleware.WithUnaryServerChain(
middleware.GrpcContext(),
middleware.GrpcRecover(),
middleware.GrpcLogger(),
))
// ...
}

验证

给FooServer新增两个方法并实现:



  • ErrorResult 返回错误

  • PanicResult 直接panic

调用结果符合预期



推荐阅读
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • FeatureRequestIsyourfeaturerequestrelatedtoaproblem?Please ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
  • 闭包一直是Java社区中争论不断的话题,很多语言都支持闭包这个语言特性,闭包定义了一个依赖于外部环境的自由变量的函数,这个函数能够访问外部环境的变量。本文以JavaScript的一个闭包为例,介绍了闭包的定义和特性。 ... [详细]
author-avatar
蓝社
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有