3. 第一个gRPC服务:从.proto到运行
好,咱们直接动手。这一章我会带你完整走一遍:写协议文件、生成代码、实现服务端和客户端,最后跑起来验证。整个过程我称之为「gRPC 的 Hello World」,但比 HTTP 的 Hello World 要扎实得多。
3.1 编写 .proto 文件:定义契约
gRPC 的核心是 协议先行。说白了,服务端和客户端都得遵守同一份 .proto 文件。我习惯把 .proto 放在项目根目录的 proto/ 文件夹下,这样结构清晰。
来看一个最简单的例子。假设我们要做一个「用户查询」服务:
syntax = "proto3";
package user;
option go_package = ".;userpb";
// 定义请求消息
message GetUserRequest {
int32 user_id = 1;
}
// 定义响应消息
message GetUserResponse {
int32 user_id = 1;
string name = 2;
int32 age = 3;
}
// 定义服务
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
}
这里有几个关键点:
- syntax = "proto3":必须写,否则默认是 proto2。proto3 更简洁,我推荐所有新项目都用它。
- package:防止命名冲突,类似 Go 的包名。
- option go_package:这个特别重要。它告诉 protoc 生成 Go 代码时的包路径。我习惯写成
.;userpb,意思是生成到当前目录,包名为userpb。 - 字段编号:每个字段后面的数字(1、2、3)是二进制编码时的标识,一旦确定就别改了。我在项目中见过有人随意改编号,结果新旧版本不兼容,排查了半天。
3.2 生成 Go 代码:protoc 命令
写好了 .proto,下一步就是生成 Go 代码。你需要安装两个工具:
protoc:Protocol Buffers 编译器protoc-gen-go-grpc:gRPC 的 Go 代码生成插件
安装命令(macOS 为例):
brew install protobuf
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/user.proto
执行后,你会看到生成了两个文件:
user.pb.go:消息结构的 Go 代码(GetUserRequest、GetUserResponse 等)user_grpc.pb.go:服务端和客户端的 gRPC 代码(接口、注册函数、客户端桩)
3.3 实现服务端:写业务逻辑
现在来实现服务端。核心就是实现 UserServiceServer 接口:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "path/to/your/proto/userpb"
)
type userServer struct {
pb.UnimplementedUserServiceServer
}
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 模拟查询数据库
return &pb.GetUserResponse{
UserId: req.UserId,
Name: "张三",
Age: 28,
}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("监听失败: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &userServer{})
log.Println("gRPC 服务启动在 :50051")
if err := s.Serve(lis); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
这里有个细节:UnimplementedUserServiceServer 是生成的空实现。你只需要覆盖自己需要的方法,其他方法会返回 Unimplemented 错误。这样设计的好处是——当你新增 RPC 方法时,旧的服务端代码不会编译报错,只会运行时返回错误。我在升级旧服务时用过这个特性,很省心。
3.4 实现客户端:发起调用
客户端代码更简单:
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "path/to/your/proto/userpb"
)
func main() {
conn, err := grpc.Dial("localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("连接失败: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: 1})
if err != nil {
log.Fatalf("调用失败: %v", err)
}
log.Printf("用户信息: ID=%d, 姓名=%s, 年龄=%d",
resp.UserId, resp.Name, resp.Age)
}
注意 grpc.WithTransportCredentials(insecure.NewCredentials())。生产环境肯定要用 TLS,但本地开发时用这个就够了。我刚开始学 gRPC 时忘了加这个参数,结果一直报连接错误,折腾了半小时才发现。
3.5 运行与测试:验证整个链路
先启动服务端:
go run server.go
再开一个终端运行客户端:
go run client.go
如果一切正常,你会看到客户端输出:
用户信息: ID=1, 姓名=张三, 年龄=28
嗯,到这里第一个 gRPC 服务就跑通了。你想想看,整个过程其实就三步:写 .proto、生成代码、实现业务逻辑。比起 RESTful API 要手动写 JSON 序列化、路由匹配,gRPC 的自动化程度高太多了。
- .proto 是服务端和客户端的「共同契约」
- protoc 生成代码,不要手动修改
- 服务端实现接口,客户端调用桩方法
- 本地测试用 insecure 连接,生产环境必须用 TLS
下一章我们会深入 .proto 的语法细节,包括各种数据类型、枚举、嵌套消息等。到时候我会分享一些我在微服务项目中踩过的坑,比如字段类型选错了导致性能问题之类的。