我正在使用Buf的connect-go库来实现gRPC服务器.

许多gRPC调用都是时间敏感的,因此它们包含一个字段,客户端使用该字段发送其当前时间戳.服务器将客户端时间戳与本地时间戳进行比较,并返回两者之间的差异.以下是.proto个定义中的一个示例:

service EventService {
    // Start performing a task
    rpc Start (StartRequest) returns (StartResponse);
}

message StartRequest {
    int64 location_id = 1;
    int64 task_id = 2;
    Location user_latlng = 3;
    google.protobuf.Timestamp now_on_device = 4;
}

message StartResponse {
    TaskPerformanceInfo info = 1;
    google.protobuf.Duration device_offset = 2;
}

因为我已经为几个RPC方法实现了这一点,所以我想看看是否可以使用interceptor来处理它,这样我就不需要确保它在所有单个RPC方法实现中都得到了处理.

由于protoc-gen-go编译器是如何为字段定义getter的,通过定义接口和使用类型断言,可以轻松地判断请求消息是否包含now_on_device字段:

type hasNowOnDevice interface {
    GetNowOnDevice() *timestamppb.Timestamp
}
if reqWithNow, ok := req.Any().(hasNowOnDevice); ok {
   // ...
}

这使得大多数拦截器非常容易编写:

func MakeDeviceTimeInterceptor() func(connect.UnaryFunc) connect.UnaryFunc {
    return connect.UnaryInterceptorFunc(
        func(next connect.UnaryFunc) connect.UnaryFunc {
            return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
                now := time.Now().UTC()
                ctxa := context.WithValue(ctx, CurrentTimestampKey{}, now)

                var deviceTimeOffset time.Duration
                // If the protobuf message has a `NowOnDevice` field, use it
                // to get the difference betweent the device time and server time.
                if reqWithNow, ok := req.Any().(hasNowOnDevice); ok {
                    deviceTime := reqWithNow.GetNowOnDevice().AsTime()
                    deviceTimeOffset = now.Sub(deviceTime)
                    ctxa = context.WithValue(ctxa, DeviceTimeDiffKey{}, deviceTimeOffset)
                }

                res, err := next(ctxa, req)

                // TODO: How do I modify the response here?

                return res, err
            })
        },
    )
}

我遇到的问题(如上所述)是如何修改响应.

我不能像对请求那样为响应定义接口,因为protoc-gen-go没有定义setter.然后我想我可以使用一个类型switch ,就像这样(上面TODO条注释):

switch resMsg := res.Any().(type) {
case *livev1.StartResponse:
    resMsg.DeviceOffset = durationpb.New(deviceTimeOffset)
    return &connect.Response[livev1.StartResponse]{
        Msg: resMsg,
    }, err
case *livev1.StatusResponse:
    resMsg.DeviceOffset = durationpb.New(deviceTimeOffset)
    return &connect.Response[livev1.StatusResponse]{
        Msg: resMsg,
    }, err
}

这种方法有三个问题:

  1. 我找不到将旧响应中的标题/预告片复制到新响应中的方法.(我认为目前还没有确定,但我不能确定.)
  2. 使用类型断言要求我对每种类型重复几乎相同的代码块.
  3. 这不再比在每个RPC方法中单独实现更简单.

是否有更简单的方法可以使用拦截器修改响应中的字段?还是有其他方法可以让我这么做?

推荐答案

Deepankar概述了一个解决方案,尽管我确实看到了将所有响应数据保存在模式定义的响应 struct 中的吸引力.如果protoc-gen-go个setter与getter一起生成,这肯定会更简单!

我找不到将旧响应中的标题/预告片复制到新响应中的方法.(我认为目前还没有确定,但我不能确定.)

你不需要这样做.在您的示例中,res.Any()返回一个指向protobuf消息的指针—您可以就地修改它.您的类型switch 可以如下所示:

switch resMsg := res.Any().(type) {
case *livev1.StartResponse:
    resMsg.DeviceOffset = durationpb.New(deviceTimeOffset)
case *livev1.StatusResponse:
    resMsg.DeviceOffset = durationpb.New(deviceTimeOffset)
}
return res, err

使用类型断言要求我对每种类型重复几乎相同的代码块.

不幸的是,您在这里的最佳 Select 可能是反射.您可以在标准Go反射或protobuf反射之间进行 Select ,两者都可以工作.对于protobuf反射,类似这样的操作应该可以做到:

res, err := next(ctx, req)
if err != nil {
    return nil, err
}
msg, ok := res.Any().(proto.Message)
if !ok {
    return res, nil
}

// Keep your logic to calculate offset!
var deviceTimeOffset time.Duration

// You could make this a global.
durationName := (*durationpb.Duration)(nil).ProtoReflect().Descriptor().FullName()

refMsg := msg.ProtoReflect()
offsetFD := refMsg.Descriptor().Fields().ByName("DeviceOffset")
if offsetFD != nil &&
    offsetFD.Message() != nil &&
    offsetFD.Message().FullName() == durationName {
    refOffset := durationpb.New(deviceTimeOffset).ProtoReflect()
    refMsg.Set(
        offsetFD, 
        protoreflect.ValueOf(refOffset),
    )
}
return res, nil

这取决于你是否认为这比重复型switch 更好或更差-它相当复杂,但它确实让事情更干燥.

Go相关问答推荐

T的Golang通用切片,其中 *T实现接口

杜松子wine -戈尼克背景在 children 围棋例行公事中被取消

如何模拟嵌入. FS?

Kafka消费者在需要时不会暂停

GitHub发布Golang子模块

使用Golang的Lambda自定义al2运行时,初始化阶段超时

一次打印用户输入的字符串n次

迭代字符串并用映射值替换原始字符串中的值的惯用方法

如果第一次匹配条件,如何跳过切片中的值

htmx 表单 + gin 无法正确读取请求正文

下载和合并时输出文件已损坏

此 Golang 程序中的同步问题

MQTT 客户端没有收到另一个客户端发送的消息

Go 中如何调用测试函数?

当函数返回一个函数时,为什么 Go 泛型会失败?

在恒等函数中将类型 T 转换为类型 U

具有两个或多个模型的 GORM 查询

为什么 0 big.Int 的 .Bytes() 值是空切片?

Go lang - 惯用的默认后备

Golang 有类似 C++ 的 decltype 的东西吗?