4. 第一个 gRPC 服务:定义 .proto 文件、生成 Go 代码、实现 Server 端、实现 Client 端、运行与测试
本章将带领你从零搭建一个完整的 gRPC 服务,涵盖契约定义、代码生成、服务端与客户端实现,以及最终的运行验证。这是后续所有权限验证功能的基础骨架。
4.1 整体流程概览
一个 gRPC 服务的开发遵循“契约优先”的原则,即先定义接口,再生成代码,最后实现业务逻辑。下图展示了本章的核心工作流:
契约优先原则: .proto 文件是服务端和客户端之间的“合同”。双方必须基于同一份 .proto 文件生成代码,否则通信将失败。这也是 gRPC 实现跨语言、跨平台通信的基础。
4.2 定义 .proto 契约文件
我们以一个简单的用户信息服务为例,定义其 RPC 方法和消息结构。
4.2.1 创建项目目录
mkdir -p grpc-user-service/proto
cd grpc-user-service
4.2.2 编写 user.proto
在 proto/ 目录下创建 user.proto 文件:
syntax = "proto3";
package userpb;
option go_package = "grpc-user-service/proto/userpb";
// 用户信息服务定义
service UserService {
// 获取用户信息
rpc GetUser (GetUserRequest) returns (GetUserResponse);
// 列出所有用户
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}
// 请求:根据用户 ID 查询
message GetUserRequest {
int32 user_id = 1;
}
// 响应:返回用户详细信息
message GetUserResponse {
int32 user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
// 请求:列表查询(支持分页)
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
// 响应:返回用户列表
message ListUsersResponse {
repeated User users = 1;
int32 total_count = 2;
}
// 用户实体
message User {
int32 user_id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
关键字段说明:
syntax = "proto3":使用 proto3 语法,字段默认省略 optional 关键字。package userpb:定义包名,用于避免命名冲突。option go_package:指定生成的 Go 代码的包路径,必须与目录结构一致。repeated:表示字段是数组(切片)类型。
4.3 生成 Go 代码
使用 protoc 编译器从 .proto 文件生成 Go 代码。你需要先安装 protoc 和 protoc-gen-go-grpc 插件。
4.3.1 安装工具
# 安装 protoc(以 Linux/macOS 为例)
# 下载地址:https://github.com/protocolbuffers/protobuf/releases
# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
4.3.2 执行代码生成
protoc --go_out=. --go-grpc_out=. proto/user.proto
执行成功后,会在 proto/userpb/ 目录下生成两个文件:
| 生成文件 | 作用 |
|---|---|
user.pb.go |
包含消息结构体(GetUserRequest、User 等)的序列化/反序列化代码 |
user_grpc.pb.go |
包含服务端接口(UserServiceServer)和客户端桩(UserServiceClient)的代码 |
常见错误: 如果
protoc 找不到插件,请确保 $GOPATH/bin 已添加到 $PATH 环境变量中。运行 which protoc-gen-go-grpc 检查插件是否可执行。
4.4 实现 Server 端
在项目根目录创建 server/main.go,实现 UserServiceServer 接口。
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "grpc-user-service/proto/userpb"
)
// 服务端实现结构体
type userServer struct {
pb.UnimplementedUserServiceServer
}
// 模拟用户数据
var users = map[int32]*pb.User{
1: {UserId: 1, Name: "Alice", Email: "alice@example.com", Age: 28},
2: {UserId: 2, Name: "Bob", Email: "bob@example.com", Age: 32},
}
// 实现 GetUser 方法
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
user, ok := users[req.UserId]
if !ok {
return nil, status.Errorf(codes.NotFound, "user not found: %d", req.UserId)
}
return &pb.GetUserResponse{
UserId: user.UserId,
Name: user.Name,
Email: user.Email,
Age: user.Age,
}, nil
}
// 实现 ListUsers 方法
func (s *userServer) ListUsers(ctx context.Context, req *pb.ListUsersRequest) (*pb.ListUsersResponse, error) {
var userList []*pb.User
for _, u := range users {
userList = append(userList, u)
}
return &pb.ListUsersResponse{
Users: userList,
TotalCount: int32(len(users)),
}, 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.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
嵌入 UnimplementedUserServiceServer: 这是 gRPC-Go 的推荐做法。当 .proto 文件新增 RPC 方法时,你的服务端不会因为未实现新方法而编译失败,而是返回一个明确的未实现错误。
4.5 实现 Client 端
在项目根目录创建 client/main.go,调用服务端接口。
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "grpc-user-service/proto/userpb"
)
func main() {
// 连接到服务端(不使用 TLS)
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()
// 调用 GetUser
resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: 1})
if err != nil {
log.Fatalf("could not get user: %v", err)
}
log.Printf("GetUser response: %+v", resp)
// 调用 ListUsers
listResp, err := client.ListUsers(ctx, &pb.ListUsersRequest{Page: 1, PageSize: 10})
if err != nil {
log.Fatalf("could not list users: %v", err)
}
log.Printf("ListUsers response: total=%d, users=%+v", listResp.TotalCount, listResp.Users)
}
4.6 运行与测试
启动服务端和客户端,验证通信是否正常。
4.6.1 启动服务端
go run server/main.go
# 输出: server listening at [::]:50051
4.6.2 启动客户端
go run client/main.go
# 输出:
# GetUser response: user_id:1 name:"Alice" email:"alice@example.com" age:28
# ListUsers response: total=2, users=[user_id:1 name:"Alice" ... user_id:2 name:"Bob" ...]
验证要点:
- 客户端成功收到服务端返回的用户数据,说明 gRPC 通信链路正常。
- 服务端日志显示连接建立和请求处理,无错误输出。
- 可以尝试修改
UserId为不存在的 ID(如 99),观察客户端收到NotFound错误。
4.7 项目结构总结
完成上述步骤后,你的项目目录结构应如下所示:
grpc-user-service/
├── proto/
│ ├── user.proto # 契约定义
│ └── userpb/
│ ├── user.pb.go # 生成的消息代码
│ └── user_grpc.pb.go # 生成的 gRPC 代码
├── server/
│ └── main.go # 服务端实现
├── client/
│ └── main.go # 客户端实现
└── go.mod # Go 模块文件
模块管理: 建议使用
go mod init grpc-user-service 初始化 Go 模块,确保导入路径与 go_package 一致。如果遇到导入错误,请检查 go.mod 中的模块名是否与代码中的导入路径匹配。
4.8 本章小结
通过本章的实战,你已经掌握了 gRPC 服务开发的核心步骤:
- 契约定义: 使用 .proto 文件描述服务接口和消息结构。
- 代码生成: 利用 protoc 工具链生成 Go 语言的桩代码。
- 服务端实现: 嵌入生成的接口,填充业务逻辑。
- 客户端调用: 通过连接和桩代码发起 RPC 调用。
- 运行验证: 启动服务端和客户端,确认通信正常。