4. 第一个 gRPC 服务:编写 .proto 文件、生成 Go 代码、实现 Server 端、实现 Client 端、运行与测试
本章将带领你从零搭建一个完整的 gRPC 服务,涵盖契约定义、代码生成、服务端与客户端实现,以及最终的运行验证。这是后续所有契约校验实践的基础骨架。
4.1 整体流程概览
一个 gRPC 服务的开发遵循“契约优先”原则,核心流程如下:
- 编写 .proto 文件:定义服务接口、请求与响应消息结构。
- 生成 Go 代码:使用 protoc 编译器及对应插件生成服务端与客户端桩代码。
- 实现 Server 端:编写业务逻辑,实现生成的接口。
- 实现 Client 端:编写客户端代码,调用远程服务。
- 运行与测试:启动服务端,运行客户端,验证通信是否正常。
提示: 本章示例使用一个简单的
UserService,包含一个 GetUser 一元 RPC 方法,用于根据用户 ID 返回用户信息。
4.2 编写 .proto 文件
在项目根目录创建 proto/user/v1/user.proto 文件,定义服务与消息结构。
syntax = "proto3";
package user.v1;
option go_package = "github.com/your-username/grpc-demo/gen/user/v1;userv1";
// 用户服务定义
service UserService {
// 获取用户信息
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
// 请求消息:用户 ID
message GetUserRequest {
string user_id = 1;
}
// 响应消息:用户信息
message GetUserResponse {
string user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
| 字段 | 说明 |
|---|---|
syntax |
指定使用 proto3 语法,必须位于文件第一行。 |
package |
定义 proto 包名,用于避免命名冲突。 |
go_package |
指定生成的 Go 代码的导入路径和包名,格式为 路径;包名。 |
service |
定义 RPC 服务接口,包含方法签名。 |
message |
定义请求与响应的数据结构,字段编号从 1 开始。 |
注意:
go_package 的路径必须与项目模块路径匹配,否则生成的代码无法正确导入。字段编号一旦确定,不应随意修改,否则会破坏向后兼容性。
4.3 生成 Go 代码
确保已安装 protoc 编译器及 Go 插件:
# 安装 protoc(macOS)
brew install protobuf
# 安装 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 --proto_path=proto \
--go_out=gen \
--go_opt=paths=source_relative \
--go-grpc_out=gen \
--go-grpc_opt=paths=source_relative \
proto/user/v1/user.proto
生成后的目录结构:
gen/
└── user/
└── v1/
├── user.pb.go # 消息序列化代码
└── user_grpc.pb.go # 服务端/客户端桩代码
| 生成文件 | 内容 |
|---|---|
user.pb.go |
包含 GetUserRequest、GetUserResponse 的结构体定义及序列化/反序列化方法。 |
user_grpc.pb.go |
包含 UserServiceServer 接口、UserServiceClient 接口及注册函数。 |
4.4 实现 Server 端
创建 server/main.go,实现 UserServiceServer 接口:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/your-username/grpc-demo/gen/user/v1"
)
// 服务端结构体
type userServer struct {
pb.UnimplementedUserServiceServer
}
// 实现 GetUser 方法
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 模拟数据库查询
return &pb.GetUserResponse{
UserId: req.UserId,
Name: "Alice",
Email: "alice@example.com",
Age: 30,
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &userServer{})
log.Println("Server listening on :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
关键点: 服务端结构体必须嵌入
pb.UnimplementedUserServiceServer,以确保即使未实现所有方法也能编译通过,并在调用时返回未实现错误。
4.5 实现 Client 端
创建 client/main.go,连接服务端并调用 GetUser 方法:
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "github.com/your-username/grpc-demo/gen/user/v1"
)
func main() {
// 建立连接(本示例使用不安全连接)
conn, err := grpc.Dial("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// 设置超时上下文
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 调用 RPC
resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "123"})
if err != nil {
log.Fatalf("could not get user: %v", err)
}
log.Printf("User Info: ID=%s, Name=%s, Email=%s, Age=%d",
resp.UserId, resp.Name, resp.Email, resp.Age)
}
提示: 生产环境应使用 TLS 加密连接,此处使用
insecure.NewCredentials() 仅用于本地开发测试。
4.6 运行与测试
启动服务端和客户端,验证通信是否正常。
- 启动服务端:在项目根目录执行
go run server/main.go,看到输出Server listening on :50051表示启动成功。 - 启动客户端:另开终端,执行
go run client/main.go,看到类似以下输出表示调用成功:
2025/01/15 10:30:00 User Info: ID=123, Name=Alice, Email=alice@example.com, Age=30
4.7 常见问题与排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
protoc: command not found |
未安装 protoc 或未添加到 PATH | 安装 protoc 并确保 protoc 命令可用 |
undefined: pb.RegisterUserServiceServer |
go_package 路径不匹配或未重新生成代码 | 检查 go_package 与项目模块路径一致,重新执行 protoc |
connection refused |
服务端未启动或端口被占用 | 确认服务端已启动,检查端口 :50051 是否被占用 |
rpc error: code = Unimplemented |
服务端未实现对应 RPC 方法 | 确保服务端结构体实现了所有接口方法 |
最佳实践: 每次修改 .proto 文件后,务必重新执行 protoc 生成代码,并重启服务端和客户端。建议将 protoc 命令写入 Makefile 或 shell 脚本,避免手动输入错误。