# 基础语法
# 头文件声明
| syntax = "proto3"; |
| |
| package user.v1; |
| |
| |
| option go_package = "github.com/yourproject/proto/user/v1;userv1"; |
| |
| import "google/protobuf/timestamp.proto"; |
# 基本数据类型映射
Protobuf 和 不同语言对应关系
- int 族、uint 族、sint 族。
- bytes:字节数组(在 Go 里面是切片)
- float 和 double:典型的浮点数
- fixed 族 和 sfixed 族:固定长度的数字类型
- bool:bool 类型
- string:字符串类型
- map<keyType,ValueType>
- repeat:数组
- optional: 可选字段 (编译后为指针)
- oneof:一个字段只能有一个值
- enum:枚举类型
# 枚举类型
| enum Status { |
| STATUS_UNSPECIFIED = 0; |
| STATUS_PENDING = 1; |
| STATUS_APPROVED = 2; |
| STATUS_REJECTED = 3; |
| } |
| |
| message Request { |
| Status status = 1; |
| } |
# 重复字段(数组)
| message UserList { |
| repeated User users = 1; |
| repeated string tags = 2; |
| repeated int32 scores = 3; |
| } |
# 映射类型
| message UserProfile { |
| map<string, string> metadata = 1; |
| map<int64, User> user_cache = 2; |
| map<string, int32> counters = 3; |
| } |
# Oneof 类型(联合类型)
| message Contact { |
| oneof contact_info { |
| string email = 1; |
| string phone = 2; |
| Address address = 3; |
| } |
| string name = 4; |
| } |
在 GO 中使用:
| contact := &Contact{ |
| Name: "Alice", |
| ContactInfo: &Contact_Email{ |
| Email: "alice@example.com", |
| }, |
| } |
# 可选字段
| message UserUpdate { |
| optional string name = 1; |
| optional int32 age = 2; |
| } |
# 时间处理
| import "google/protobuf/timestamp.proto"; |
| |
| message Event { |
| string name = 1; |
| google.protobuf.Timestamp created_at = 2; |
| google.protobuf.Timestamp updated_at = 3; |
| } |
Go 代码:
| import "google.golang.org/protobuf/types/known/timestamppb" |
| |
| |
| event := &Event{ |
| Name: "user_login", |
| CreatedAt: timestamppb.Now(), |
| UpdatedAt: timestamppb.New(time.Now()), |
| } |
| |
| |
| goTime := event.CreatedAt.AsTime() |
# message 定义
要给每个字段指定编号,编号从 1 开始,编号不能重复
字段编号用于序列化
| message Name { |
| int64 id = 1; |
| string name = 2; |
| string email = 3; |
| int32 age = 4; |
| |
| } |
- 每个字段必须有唯一的编号(1-15 用一个字节编码,建议常用字段使用)
- 字段编号一旦使用就不能更改(向后兼容)
- 编号范围:1-536870911(除了 19000-19999)
# Service 定义
- 使用 Service 关键字
- 使用 RPC 关键字来定义一个方法
- 每个方法都使用一个 message 来作为输入,以及一个 message 来作为一个输出
服务定义
| service UserService { |
| |
| rpc GetUser(GetUserRequest) returns (User); |
| |
| |
| rpc ListUsers(ListUsersRequest) returns (stream User); |
| |
| |
| rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse); |
| |
| |
| rpc ChatUsers(stream ChatMessage) returns (stream ChatMessage); |
| } |
# 简单 RPC
简单调用是最基本的 gRPC 通信模式,类似于传统的函数调用:
特点:
- 客户端发送一个请求,服务端返回一个响应
- 请求 - 响应的一对一通信模式
- 同步阻塞调用(客户端等待服务端响应)
- 数据一次性传输,适合较小的数据量
服务端实现
| func (s *userServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) { |
| |
| return &pb.User{ |
| Id: req.UserId, |
| Name: "Alice", |
| Email: "alice@example.com", |
| }, nil |
| } |
客户端实现
| |
| user, err := client.GetUser(context.Background(), &pb.GetUserRequest{ |
| UserId: 123, |
| }) |
# 流式调用 (Streaming RPC)
流式调用允许客户端和服务端通过数据流进行双向通信,分为三种类型:
# 服务端流式 RPC
服务端可以发送多个响应给客户端:
特点:
- 客户端发送一个请求
- 服务端返回一个响应流(多个响应)
- 适用于需要返回大量数据或实时更新的场景
服务端实现
| func (s *userServiceServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error { |
| |
| for i := 0; i < 10; i++ { |
| user := &pb.User{Id: int64(i), Name: fmt.Sprintf("User%d", i)} |
| if err := stream.Send(user); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
客户端实现
| |
| stream, err := client.ListUsers(context.Background(), &pb.ListUsersRequest{}) |
| for { |
| user, err := stream.Recv() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| log.Fatal(err) |
| } |
| fmt.Printf("Received user: %+v\n", user) |
| } |
# 客户端流式 RPC
客户端可以发送多个请求给服务端:
特点:
- 客户端发送一个请求流(多个请求)
- 服务端返回一个响应
- 适用于客户端需要发送大量数据的场景
服务端实现
| func (s *userServiceServer) CreateUsers(stream pb.UserService_CreateUsersServer) error { |
| var users []*pb.User |
| |
| for { |
| req, err := stream.Recv() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| return err |
| } |
| |
| |
| user := &pb.User{ |
| Name: req.Name, |
| Email: req.Email, |
| } |
| users = append(users, user) |
| } |
| |
| |
| return stream.SendAndClose(&pb.CreateUsersResponse{ |
| Users: users, |
| CreatedCount: int32(len(users)), |
| }) |
| } |
客户端实现
| stream, err := client.CreateUsers(context.Background()) |
| if err != nil { |
| log.Fatal(err) |
| } |
| |
| |
| usersToCreate := []struct{ name, email string }{ |
| {"Alice", "alice@example.com"}, |
| {"Bob", "bob@example.com"}, |
| } |
| |
| for _, u := range usersToCreate { |
| err := stream.Send(&pb.CreateUserRequest{ |
| Name: u.name, |
| Email: u.email, |
| }) |
| if err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| |
| response, err := stream.CloseAndRecv() |
# 双向流式 RPC
客户端和服务端都可以发送多个消息:
特点:
- 客户端和服务端都可以发送多个消息
- 两个独立的流,可以独立操作
- 适用于实时通信场景
服务端实现
| func (s *userServiceServer) ChatUsers(stream pb.UserService_ChatUsersServer) error { |
| |
| msgChan := make(chan *pb.ChatMessage, 10) |
| |
| |
| go func() { |
| defer close(msgChan) |
| for { |
| msg, err := stream.Recv() |
| if err == io.EOF { |
| return |
| } |
| if err != nil { |
| log.Printf("Error receiving message: %v", err) |
| return |
| } |
| |
| |
| msgChan <- msg |
| } |
| }() |
| |
| |
| for msg := range msgChan { |
| |
| time.Sleep(500 * time.Millisecond) |
| |
| |
| response := &pb.ChatMessage{ |
| UserId: msg.UserId, |
| Content: fmt.Sprintf("Echo: %s", msg.Content), |
| Time: timestamppb.Now(), |
| } |
| |
| |
| if err := stream.Send(response); err != nil { |
| log.Printf("Error sending message: %v", err) |
| return err |
| } |
| |
| log.Printf("Processed message from user %d: %s", msg.UserId, msg.Content) |
| } |
| |
| return nil |
| } |
客户端实现
| func (c *Client) ChatExample() { |
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
| defer cancel() |
| |
| |
| stream, err := c.client.ChatUsers(ctx) |
| if err != nil { |
| log.Fatalf("Failed to create chat stream: %v", err) |
| } |
| |
| |
| sendChan := make(chan string, 5) |
| done := make(chan struct{}) |
| |
| |
| go func() { |
| defer close(sendChan) |
| messages := []string{"Hello!", "How are you?", "Tell me a joke", "Goodbye!"} |
| |
| for _, msg := range messages { |
| select { |
| case sendChan <- msg: |
| |
| chatMsg := &pb.ChatMessage{ |
| UserId: 1, |
| Content: msg, |
| Time: timestamppb.Now(), |
| } |
| |
| if err := stream.Send(chatMsg); err != nil { |
| log.Printf("Failed to send: %v", err) |
| return |
| } |
| log.Printf("Sent: %s", msg) |
| time.Sleep(2 * time.Second) |
| case <-done: |
| return |
| } |
| } |
| |
| |
| stream.CloseSend() |
| }() |
| |
| |
| for { |
| response, err := stream.Recv() |
| if err == io.EOF { |
| log.Println("Stream closed by server") |
| close(done) |
| break |
| } |
| if err != nil { |
| log.Printf("Error receiving: %v", err) |
| close(done) |
| break |
| } |
| |
| log.Printf("Received: %s (at %v)", response.Content, response.Time.AsTime()) |
| } |
| |
| |
| <-sendChan |
| } |
| 特性 |
简单调用 |
服务端流式 |
客户端流式 |
双向流式 |
| 客户端请求 |
单个 |
单个 |
多个 |
多个 |
| 服务端响应 |
当个 |
多个 |
单个 |
多个 |
| 通信模式 |
请求 - 响应 |
推动 |
批量发送 |
双向通信 |
| 适用场景 |
简单查询 |
列表获取 |
大量数据上传 |
实时通信 |
请求和响应消息
| message GetUserRequest { |
| int64 user_id = 1; |
| } |
| |
| message ListUsersRequest { |
| int32 page_size = 1; |
| string page_token = 2; |
| } |
| |
| message CreateUserRequest { |
| string name = 1; |
| string email = 2; |
| } |
| |
| message CreateUsersResponse { |
| repeated User users = 1; |
| int32 created_count = 2; |
| } |
# Go 代码生成和使用
# 1. 安装工具
| # 安装 protoc 编译器 |
| brew install protobuf # macOS |
| |
| # 安装 Go 插件 |
| go install google.golang.org/protobuf/cmd/protoc-gen-go@latest |
| go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest |
# 2. 编译 proto 文件
| protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative user.proto |
| |
| |
| |
| |
| |
# 3. Go 服务端实现
| type userServiceServer struct { |
| pb.UnimplementedUserServiceServer |
| } |
| |
| func (s *userServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) { |
| |
| return &pb.User{ |
| Id: req.UserId, |
| Name: "Alice", |
| Email: "alice@example.com", |
| }, nil |
| } |
| |
| func (s *userServiceServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error { |
| |
| for i := 0; i < 10; i++ { |
| user := &pb.User{Id: int64(i), Name: fmt.Sprintf("User%d", i)} |
| if err := stream.Send(user); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
# 4. Go 客户端使用
| conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) |
| if err != nil { |
| log.Fatal(err) |
| } |
| defer conn.Close() |
| |
| client := pb.NewUserServiceClient(conn) |
| |
| |
| user, err := client.GetUser(context.Background(), &pb.GetUserRequest{ |
| UserId: 123, |
| }) |
| |
| |
| stream, err := client.ListUsers(context.Background(), &pb.ListUsersRequest{}) |
| for { |
| user, err := stream.Recv() |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| log.Fatal(err) |
| } |
| fmt.Printf("Received user: %+v\n", user) |
| } |
# 序列化操作
# 1. 二进制序列化
| import "google.golang.org/protobuf/proto" |
| |
| user := &pb.User{ |
| Id: 1, |
| Name: "Alice", |
| Email: "alice@example.com", |
| } |
| |
| |
| data, err := proto.Marshal(user) |
| if err != nil { |
| log.Fatal(err) |
| } |
| |
| |
| newUser := &pb.User{} |
| err = proto.Unmarshal(data, newUser) |
| if err != nil { |
| log.Fatal(err) |
| } |
# 2. JSON 序列化
| import "google.golang.org/protobuf/encoding/protojson" |
| |
| |
| jsonData, err := protojson.Marshal(user) |
| if err != nil { |
| log.Fatal(err) |
| } |
| |
| |
| jsonUser := &pb.User{} |
| err = protojson.Unmarshal(jsonData, jsonUser) |
| if err != nil { |
| log.Fatal(err) |
| } |
# 最佳实践
# 1. 字段编号规划
| message User { |
| |
| int64 id = 1; |
| string name = 2; |
| string email = 3; |
| |
| |
| string description = 16; |
| map<string, string> metadata = 17; |
| } |
# 2. 保留字段
| message User { |
| |
| reserved 4, 5, 8 to 15; |
| |
| |
| reserved "old_field", "deprecated_field"; |
| |
| int64 id = 1; |
| string name = 2; |
| string email = 3; |
| } |
# 3. 版本兼容性
安全的修改:
- ✅ 添加新字段
- ✅ 删除字段(需要保留字段编号)
- ✅ 重命名字段(不影响序列化)
危险的修改:
- ❌ 更改字段类型
- ❌ 更改字段编号
- ❌ 重用已保留的字段编号
# 4. 项目结构建议
project/
├── proto/
│ ├── user/
│ │ └── v1/
│ │ ├── user.proto
│ │ ├── user.pb.go
│ │ └── user_grpc.pb.go
│ └── order/
│ └── v1/
│ ├── order.proto
│ ├── order.pb.go
│ └── order_grpc.pb.go
├── cmd/
│ ├── server/
│ └── client/
└── internal/
└── service/
# 5. Makefile
.PHONY: proto clean build
proto:
find proto -name "*.proto" -exec protoc \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
{} \;
clean:
find . -name "*.pb.go" -delete
build: proto
go build -o bin/server cmd/server/main.go
go build -o bin/client cmd/client/main.go
# 6. 常见问题
# Q1: 字段编号用完了怎么办?
A: 字段编号范围是 1 到 536,870,911,正常使用不会用完。如果真的不够,考虑拆分消息。
# Q2: 如何处理大文件?
A: protobuf 不适合大文件,建议:
- 使用
bytes 字段存储文件引用(URL)
- 将大文件存储在对象存储中
- 分块传输大数据
# Q3: protobuf vs JSON 如何选择?
A:
- protobuf: 性能要求高、内部服务通信、需要强类型
- JSON: 调试友好、前端交互、第三方集成
# Q4: 如何调试 protobuf 数据?
A:
bash
| |
| echo "binary_data" | protoc --decode=User user.proto |
| |
| |
| jsonData, _ := protojson.Marshal(protoMessage) |
| fmt.Println(string(jsonData)) |
# 7. 记住要点
- 🔢 字段编号一旦分配不要更改
- 🎯 1-15 编号给常用字段(节省字节)
- 🛡️ 删除字段要用
reserved 保留编号
- 📦 利用 oneof 节省内存和带宽
- ⏰ 时间字段使用
google.protobuf.Timestamp
# 扩展阅读
- Protocol Buffers 官方文档
- gRPC Go 快速开始
- Protocol Buffers 语言指南