4. 第一个 gRPC 服务:定义 .proto 文件、生成 Go 代码、实现 Server 端、实现 Client 端、运行与测试

本章将带领你从零搭建一个完整的 gRPC 服务,涵盖契约定义、代码生成、服务端与客户端实现,以及最终的运行验证。这是后续所有权限验证功能的基础骨架。

4.1 整体流程概览

一个 gRPC 服务的开发遵循“契约优先”的原则,即先定义接口,再生成代码,最后实现业务逻辑。下图展示了本章的核心工作流:

gRPC 服务开发全流程 gRPC 服务开发全流程 定义 .proto 契约文件 protoc 编译 生成 .pb.go 实现 Server 业务逻辑 实现 Client 调用服务 运行与测试 验证通信 💡 关键路径 Server 和 Client 可并行开发 但必须基于同一份 .proto 契约
契约优先原则: .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 模块文件
gRPC 用户服务项目结构 gRPC 用户服务项目结构 grpc-user-service/ proto/ server/ client/ user.proto userpb/ (生成代码) main.go main.go
模块管理: 建议使用 go mod init grpc-user-service 初始化 Go 模块,确保导入路径与 go_package 一致。如果遇到导入错误,请检查 go.mod 中的模块名是否与代码中的导入路径匹配。

4.8 本章小结

通过本章的实战,你已经掌握了 gRPC 服务开发的核心步骤:

  • 契约定义: 使用 .proto 文件描述服务接口和消息结构。
  • 代码生成: 利用 protoc 工具链生成 Go 语言的桩代码。
  • 服务端实现: 嵌入生成的接口,填充业务逻辑。
  • 客户端调用: 通过连接和桩代码发起 RPC 调用。
  • 运行验证: 启动服务端和客户端,确认通信正常。