4. Go gRPC 客户端实现:基于 Protobuf 桩代码的 RPC 调用

在完成 .proto 文件定义并通过 protoc 生成 Go 桩代码后,客户端开发者无需关心网络传输细节,只需调用生成的桩代码即可发起 RPC 调用。本章聚焦于两种最常用的通信模式:Unary RPC(一元调用)与 Client Streaming RPC(客户端流式调用),并给出完整的 Go 实现示例。

4.1 客户端桩代码结构概览

假设我们定义了一个 order.proto 文件,包含以下两个 RPC 方法:

service OrderService {
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
  rpc BatchSubmitOrders (stream Order) returns (BatchSummary);
}

执行 protoc 后,生成的 Go 桩代码主要包含以下核心类型:

桩代码元素 说明
OrderServiceClient 客户端接口,定义所有 RPC 方法签名
orderServiceClient 接口的具体实现,封装 gRPC 连接与调用逻辑
NewOrderServiceClient(cc grpc.ClientConnInterface) 构造函数,传入连接对象返回客户端实例
CreateOrder(ctx, req, opts...) Unary RPC 调用方法,返回响应与错误
BatchSubmitOrders(ctx, opts...) Client Streaming RPC 调用方法,返回流对象
OrderService_BatchSubmitOrdersClient 客户端流接口,提供 Send、CloseAndRecv 方法
提示: 桩代码中的 opts ...grpc.CallOption 参数可用于传递超时、元数据、压缩等调用选项,在压测场景中常用来设置 grpc.WaitForReady(true) 或自定义拦截器。

4.2 Unary RPC 调用实现

Unary RPC 是最简单的请求-响应模式,客户端发送一个请求,服务端返回一个响应。实现步骤如下:

  1. 建立连接:使用 grpc.Dial 创建到服务端的连接。
  2. 创建客户端:调用 pb.NewOrderServiceClient 传入连接。
  3. 构造请求:填充 CreateOrderRequest 消息字段。
  4. 发起调用:调用 client.CreateOrder 并处理响应。
package main

import (
    "context"
    "log"
    "time"

    pb "path/to/proto/gen/go/order/v1"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

func main() {
    // 1. 建立连接(压测时建议使用 grpc.WithBlock 确保连接就绪)
    conn, err := grpc.Dial(
        "localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithBlock(),
        grpc.WithTimeout(5*time.Second),
    )
    if err != nil {
        log.Fatalf("failed to dial: %v", err)
    }
    defer conn.Close()

    // 2. 创建客户端
    client := pb.NewOrderServiceClient(conn)

    // 3. 构造请求
    req := &pb.CreateOrderRequest{
        UserId:  "user_1001",
        Sku:     "SKU-2024-001",
        Quantity: 2,
    }

    // 4. 发起 Unary 调用(设置 3 秒超时上下文)
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    resp, err := client.CreateOrder(ctx, req)
    if err != nil {
        log.Fatalf("CreateOrder failed: %v", err)
    }

    log.Printf("order created: id=%s, status=%s", resp.OrderId, resp.Status)
}
压测要点: 在性能测试中,grpc.Dial 应复用连接,避免每次请求都新建连接。同时建议使用 grpc.WithDefaultCallOptions(grpc.WaitForReady(true)) 让客户端在连接暂时不可用时等待,而非立即失败。

4.3 Client Streaming RPC 调用实现

Client Streaming RPC 允许客户端发送多条消息,最后关闭流并等待服务端返回汇总响应。典型场景包括批量提交订单、上传日志等。实现步骤:

  1. 获取流对象:调用 client.BatchSubmitOrders 返回 OrderService_BatchSubmitOrdersClient。
  2. 发送消息:循环调用 stream.Send(msg) 发送每条数据。
  3. 关闭并接收:调用 stream.CloseAndRecv() 结束发送并获取服务端响应。
func batchSubmitOrders(client pb.OrderServiceClient, orders []*pb.Order) (*pb.BatchSummary, error) {
    // 1. 获取流对象
    stream, err := client.BatchSubmitOrders(context.Background())
    if err != nil {
        return nil, err
    }

    // 2. 循环发送每条订单
    for _, order := range orders {
        if err := stream.Send(order); err != nil {
            // 发送失败时,可尝试 Recv 获取服务端错误
            return nil, err
        }
    }

    // 3. 关闭发送端并接收汇总响应
    summary, err := stream.CloseAndRecv()
    if err != nil {
        return nil, err
    }
    return summary, nil
}

// 调用示例
func main() {
    conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    defer conn.Close()
    client := pb.NewOrderServiceClient(conn)

    orders := []*pb.Order{
        {UserId: "u1", Sku: "A001", Quantity: 1},
        {UserId: "u2", Sku: "B002", Quantity: 3},
        {UserId: "u3", Sku: "C003", Quantity: 5},
    }

    summary, err := batchSubmitOrders(client, orders)
    if err != nil {
        log.Fatalf("batch submit failed: %v", err)
    }
    log.Printf("batch summary: total=%d, success=%d, failed=%d",
        summary.TotalCount, summary.SuccessCount, summary.FailedCount)
}
注意: Client Streaming 中,stream.Send 是阻塞的,受 gRPC 流量控制和网络延迟影响。压测时需关注 Send 的耗时分布,避免因发送过慢导致整体吞吐下降。建议在发送循环中增加批量大小控制或并发发送。

4.4 两种 RPC 模式对比

对比维度 Unary RPC Client Streaming RPC
消息方向 单请求 → 单响应 多请求 → 单响应
适用场景 查询、创建单条资源 批量提交、日志上传、数据导入
客户端 API client.Method(ctx, req) stream.Send() + stream.CloseAndRecv()
错误处理 调用返回时直接获取 error Send 阶段错误需主动检查;CloseAndRecv 返回最终错误
压测关注点 QPS、延迟、连接复用 发送速率、流控、内存占用

4.5 客户端调用流程总览

下图展示了从建立连接到完成两种 RPC 调用的完整流程:

Go gRPC 客户端调用流程 Go gRPC 客户端调用流程 grpc.Dial() NewOrderServiceClient(conn) Unary: client.CreateOrder() 等待响应 + 处理 error Stream: BatchSubmitOrders() 循环 stream.Send(order) CloseAndRecv() 获取汇总 说明:Unary 调用直接返回响应;Client Streaming 需先发送多条消息,再关闭流获取汇总结果

4.6 客户端调用最佳实践

  • 连接复用:一个 grpc.ClientConn 可被多个 goroutine 并发使用,压测时只需建立少量连接即可支撑高并发。
  • 超时控制:始终使用带超时的 context.Context,避免因服务端异常导致客户端 goroutine 泄漏。
  • 错误处理:Unary 调用直接检查 error;Client Streaming 中 Send 返回 error 后应调用 stream.RecvMsg 或 CloseAndRecv 获取服务端详细错误。
  • 流控感知:Client Streaming 发送速率受 gRPC 流控窗口限制,压测时需监控 Send 的阻塞时间,必要时调整 InitialWindowSize 参数。
  • 拦截器注入:通过 grpc.WithUnaryInterceptor 和 grpc.WithStreamInterceptor 可注入客户端拦截器,用于统一日志、指标采集和重试逻辑。
压测建议: 在编写压测脚本时,建议将 Unary 和 Client Streaming 分别封装为独立的 Runner 结构体,便于后续在测试框架中按场景组合调用。同时,使用 sync.Pool 复用请求消息对象,减少 GC 压力。