3. 第一个 gRPC 服务:定义 .proto 文件、生成 Go 代码、实现简单的 Unary RPC 服务端与客户端

构建 gRPC 服务的起点是 契约先行。开发者首先使用 Protocol Buffers(protobuf)语言在 .proto 文件中定义服务接口与消息结构,然后通过 protoc 编译器生成指定语言的代码骨架。本节将带领你完成从 .proto 定义到 Go 语言 Unary RPC 服务端与客户端的完整实战。

3.1 契约先行:定义 .proto 文件

在项目根目录下创建 proto/order/v1/order.proto 文件。该文件定义了订单查询服务的消息格式与 RPC 方法。

syntax = "proto3";

package order.v1;

option go_package = "github.com/yourorg/ordergrpc/api/order/v1;orderpb";

// 订单查询服务
service OrderQueryService {
  // 根据订单ID查询订单详情(Unary RPC)
  rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
}

// 请求消息:订单ID
message GetOrderRequest {
  string order_id = 1;
}

// 响应消息:订单详情
message GetOrderResponse {
  string order_id = 1;
  string user_id = 2;
  repeated OrderItem items = 3;
  double total_amount = 4;
  OrderStatus status = 5;
  int64 created_at = 6; // Unix 时间戳
}

// 订单商品条目
message OrderItem {
  string product_id = 1;
  string product_name = 2;
  int32 quantity = 3;
  double unit_price = 4;
}

// 订单状态枚举
enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_PENDING = 1;
  ORDER_STATUS_CONFIRMED = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_DELIVERED = 4;
  ORDER_STATUS_CANCELLED = 5;
}
契约要点:字段编号(如 order_id = 1)是 protobuf 二进制编码的唯一标识,一旦发布不应修改。枚举的第一个值必须为 0,用于默认值兼容。

3.2 生成 Go 代码

确保已安装 protoc 编译器及 Go 插件:

# 安装 protoc(略)
# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

在项目根目录执行以下命令生成代码:

protoc --go_out=. \
       --go-grpc_out=. \
       proto/order/v1/order.proto

生成的文件结构如下:

生成文件 内容说明
api/order/v1/order.pb.go 消息结构体(GetOrderRequest、GetOrderResponse 等)的序列化/反序列化代码
api/order/v1/order_grpc.pb.go 服务端接口、客户端桩代码、注册函数
生成后检查:打开 order_grpc.pb.go,确认其中包含 OrderQueryServiceServer 接口和 RegisterOrderQueryServiceServer 函数。

3.3 实现 Unary RPC 服务端

创建 server/main.go,实现 OrderQueryServiceServer 接口:

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"

    pb "github.com/yourorg/ordergrpc/api/order/v1"
)

type orderServer struct {
    pb.UnimplementedOrderQueryServiceServer
}

// GetOrder 实现 Unary RPC 方法
func (s *orderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.GetOrderResponse, error) {
    log.Printf("收到查询请求: order_id=%s", req.OrderId)

    // 模拟数据库查询,返回固定订单
    return &pb.GetOrderResponse{
        OrderId:     req.OrderId,
        UserId:      "user_001",
        TotalAmount: 299.99,
        Status:      pb.OrderStatus_ORDER_STATUS_CONFIRMED,
        CreatedAt:   1700000000,
        Items: []*pb.OrderItem{
            {ProductId: "prod_01", ProductName: "Go 并发编程实战", Quantity: 1, UnitPrice: 99.99},
            {ProductId: "prod_02", ProductName: "gRPC 微服务设计", Quantity: 2, UnitPrice: 100.00},
        },
    }, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("监听失败: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterOrderQueryServiceServer(s, &orderServer{})
    reflection.Register(s) // 开启反射,便于调试

    log.Println("gRPC 服务端启动于 :50051")
    if err := s.Serve(lis); err != nil {
        log.Fatalf("服务运行失败: %v", err)
    }
}
关键实践:必须嵌入 pb.UnimplementedOrderQueryServiceServer,确保未来服务接口新增方法时,现有代码仍能编译通过。

3.4 实现 Unary RPC 客户端

创建 client/main.go,建立连接并调用 GetOrder:

package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"

    pb "github.com/yourorg/ordergrpc/api/order/v1"
)

func main() {
    // 建立连接(开发环境使用不安全连接)
    conn, err := grpc.Dial("localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithBlock(),
    )
    if err != nil {
        log.Fatalf("连接失败: %v", err)
    }
    defer conn.Close()

    client := pb.NewOrderQueryServiceClient(conn)

    // 设置超时上下文
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // 发起 Unary RPC 调用
    resp, err := client.GetOrder(ctx, &pb.GetOrderRequest{OrderId: "ORD-2024-001"})
    if err != nil {
        log.Fatalf("RPC 调用失败: %v", err)
    }

    log.Printf("订单ID: %s", resp.OrderId)
    log.Printf("用户ID: %s", resp.UserId)
    log.Printf("总金额: %.2f", resp.TotalAmount)
    log.Printf("状态: %s", resp.Status.String())
    for _, item := range resp.Items {
        log.Printf("  商品: %s x%d = %.2f", item.ProductName, item.Quantity, item.UnitPrice*float64(item.Quantity))
    }
}

3.5 运行验证

  1. 启动服务端:go run server/main.go
  2. 启动客户端:go run client/main.go
  3. 预期输出:客户端打印订单详情,服务端输出收到请求的日志

3.6 Unary RPC 调用流程

下图展示了从客户端发起请求到服务端返回响应的完整 Unary RPC 调用流程:

Unary RPC 调用流程 Unary RPC 调用流程 Go 客户端 GetOrderRequest Go 服务端 GetOrderResponse gRPC 框架层 HTTP/2 传输 流程说明 1. 客户端构造 GetOrderRequest 并调用 Stub 方法 2. gRPC 框架将请求序列化为 protobuf 二进制格式 3. 通过 HTTP/2 发送到服务端 4. 服务端反序列化,执行业务逻辑,返回 GetOrderResponse 5. 响应沿相同路径返回客户端,反序列化后交付给调用方

3.7 契约与代码的映射关系

下表总结了 .proto 定义与生成 Go 代码之间的关键映射:

.proto 元素 生成 Go 类型/函数 使用位置
service OrderQueryService OrderQueryServiceServer 接口
OrderQueryServiceClient 接口
服务端实现接口;客户端通过 Client 调用
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse) GetOrder(context.Context, *GetOrderRequest) (*GetOrderResponse, error) 服务端方法签名;客户端 client.GetOrder(ctx, req)
message GetOrderRequest type GetOrderRequest struct 含 OrderId string 请求参数构造与解析
enum OrderStatus type OrderStatus int32 及常量 状态字段赋值与比较
repeated OrderItem items Items []*OrderItem 切片操作,支持 append 和遍历

3.8 常见陷阱与最佳实践

  • 字段编号不可复用:删除字段后应保留编号并标记 reserved,避免未来误用导致数据损坏。
  • 枚举默认值:客户端未显式赋值时,枚举字段默认为 0,因此第一个枚举值必须为 UNSPECIFIED 或类似语义。
  • 超时控制:客户端必须设置 context.WithTimeout,防止服务端异常导致客户端永久阻塞。
  • 错误处理:gRPC 返回 status.Status 错误,使用 status.Code(err) 判断错误类型,而非简单比较 err != nil。
  • 连接复用:客户端应复用 *grpc.ClientConn,避免每次调用都创建新连接。
实战检查清单:
  1. .proto 文件是否包含 go_package 选项?
  2. 服务端是否嵌入了 Unimplemented*Server?
  3. 客户端是否使用了 insecure.NewCredentials()(仅开发环境)?
  4. 是否设置了合理的 RPC 超时时间?

至此,你已经完成了第一个 gRPC 服务的完整开发流程。从契约定义到代码生成,再到服务端与客户端的实现,这一模式将贯穿整个微服务架构的设计与开发。