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 运行验证
- 启动服务端:
go run server/main.go - 启动客户端:
go run client/main.go - 预期输出:客户端打印订单详情,服务端输出收到请求的日志
3.6 Unary RPC 调用流程
下图展示了从客户端发起请求到服务端返回响应的完整 Unary RPC 调用流程:
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,避免每次调用都创建新连接。
实战检查清单:
.proto文件是否包含go_package选项?- 服务端是否嵌入了
Unimplemented*Server? - 客户端是否使用了
insecure.NewCredentials()(仅开发环境)? - 是否设置了合理的 RPC 超时时间?
至此,你已经完成了第一个 gRPC 服务的完整开发流程。从契约定义到代码生成,再到服务端与客户端的实现,这一模式将贯穿整个微服务架构的设计与开发。