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)是二进制编码时的标识,一旦确定就别改了。我在项目中见过有人随意改编号,结果新旧版本不兼容,排查了半天。
小技巧:字段编号 1-15 只占 1 个字节,频繁使用的字段尽量用小编号。16 以上就要 2 个字节了。

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 代码(接口、注册函数、客户端桩)
注意:生成的代码不要手动修改!每次修改 .proto 后重新生成即可。我曾经见过有人直接改生成的 .go 文件,结果下次重新生成时全丢了,白忙活一场。

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 的语法细节,包括各种数据类型、枚举、嵌套消息等。到时候我会分享一些我在微服务项目中踩过的坑,比如字段类型选错了导致性能问题之类的。