4. 定义第一个gRPC服务:编写hello.proto、生成Go代码、理解.pb.go与_grpc.pb.go文件结构

从零开始构建一个gRPC服务,第一步是定义契约(Contract)。在gRPC中,契约由Protocol Buffers(protobuf)描述,后缀为.proto。本章将带领你完成从编写hello.proto到生成Go代码,并深入剖析生成的两个核心文件——.pb.go与_grpc.pb.go——的结构与职责。

4.1 编写第一个Proto文件:hello.proto

在项目根目录下创建proto/hello.proto文件。一个标准的gRPC服务定义包含:包声明、服务接口、请求/响应消息。

syntax = "proto3";

package hello;

option go_package = "github.com/yourname/grpc-demo/proto/hello;hellopb";

// HelloService 定义了一个简单的问候服务
service HelloService {
  // 一元RPC:发送一个名字,返回问候语
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

// HelloRequest 请求消息,包含用户名称
message HelloRequest {
  string name = 1;
}

// HelloResponse 响应消息,包含问候内容
message HelloResponse {
  string greeting = 1;
}
关键点:option go_package 决定了生成的Go代码的包路径和包名。格式为 路径;包名,例如 proto/hello;hellopb 表示生成的文件在 proto/hello 目录下,包名为 hellopb。

4.2 生成Go代码:protoc 命令详解

使用 protoc 编译器并配合 protoc-gen-go 和 protoc-gen-go-grpc 插件,从 .proto 生成Go代码。

前置条件:确保已安装 protoc、protoc-gen-go、protoc-gen-go-grpc,并将 $GOPATH/bin 加入 PATH。

执行命令:

protoc --go_out=. --go-grpc_out=. proto/hello.proto

参数说明:

参数 作用 生成文件
--go_out 指定 protobuf 消息的Go代码输出目录 hello.pb.go
--go-grpc_out 指定 gRPC 服务端/客户端桩代码输出目录 hello_grpc.pb.go
proto/hello.proto 输入的 proto 文件路径 —

执行后,在 proto/hello/ 目录下会生成两个文件:

  • hello.pb.go
  • hello_grpc.pb.go

4.3 理解 .pb.go 文件结构

hello.pb.go 是 protobuf 消息层 的Go实现。它负责数据的序列化、反序列化以及字段访问。

核心内容:

  • 消息结构体: HelloRequest 和 HelloResponse 被生成为Go结构体,字段带有protobuf标签。
  • Getter 方法: 每个字段都有对应的 GetXxx() 方法,安全访问字段值(避免nil panic)。
  • 序列化/反序列化: 实现了 proto.Marshal、proto.Unmarshal 接口,支持二进制编码。
  • 注册函数: init() 函数将消息类型注册到全局protobuf注册表中。
// 生成的 HelloRequest 结构体示例
type HelloRequest struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}

func (x *HelloRequest) GetName() string {
    if x != nil {
        return x.Name
    }
    return ""
}

4.4 理解 _grpc.pb.go 文件结构

hello_grpc.pb.go 是 gRPC 服务层 的Go实现。它定义了服务端接口、客户端桩代码以及注册函数。

核心内容:

组件 说明
HelloServiceServer 接口 服务端必须实现的接口,包含 SayHello 方法。
HelloServiceClient 接口 客户端调用的接口,提供 SayHello 方法。
RegisterHelloServiceServer 将服务实现注册到 gRPC 服务器。
helloServiceClient 结构体 客户端桩的具体实现,封装了 grpc.ClientConn 调用。
UnimplementedHelloServiceServer 提供默认实现,便于服务端只覆盖部分方法(向前兼容)。
// 生成的 HelloServiceServer 接口
type HelloServiceServer interface {
    SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
    mustEmbedUnimplementedHelloServiceServer()
}

// 生成的 HelloServiceClient 接口
type HelloServiceClient interface {
    SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
}

4.5 两个文件的职责边界与协作关系

下图展示了 .pb.go 与 _grpc.pb.go 在gRPC调用链路中的位置与分工:

.pb.go 与 _grpc.pb.go 职责边界 hello.pb.go 消息层(数据契约) • HelloRequest 结构体 • HelloResponse 结构体 • GetName() / GetGreeting() • Marshal / Unmarshal • proto.RegisterType • 字段标签与序列化 hello_grpc.pb.go 服务层(通信契约) • HelloServiceServer 接口 • HelloServiceClient 接口 • RegisterHelloServiceServer • helloServiceClient 实现 • Unimplemented 默认实现 • gRPC 方法描述 方法签名引用消息类型 SayHello(HelloRequest) → HelloResponse 💡 消息层定义数据格式,服务层定义通信行为,两者共同构成完整契约

4.6 生成文件清单与使用场景

文件 生成命令 开发者需要修改? 典型使用场景
hello.pb.go --go_out ❌ 不修改,由proto驱动 服务端/客户端引用消息结构体
hello_grpc.pb.go --go-grpc_out ❌ 不修改,由proto驱动 服务端实现接口,客户端创建桩
重要原则:永远不要手动编辑 .pb.go 或 _grpc.pb.go 文件。当proto文件变更时,重新运行 protoc 命令覆盖生成即可。手动修改会导致与proto契约不同步,引发难以排查的运行时错误。

4.7 验证生成结果

在Go代码中导入生成的包,验证编译通过:

package main

import (
    "fmt"
    "github.com/yourname/grpc-demo/proto/hello"
)

func main() {
    req := &hellopb.HelloRequest{Name: "Alice"}
    fmt.Println(req.GetName()) // 输出: Alice
}

如果编译成功,说明proto定义正确,代码生成无误。至此,你已经完成了第一个gRPC服务的契约定义与代码生成,为后续实现服务端和客户端打下了坚实基础。