# 基础语法

# 头文件声明

syntax = "proto3";
package user.v1;
// Go 包路径配置
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;  // 必须有 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;      // ID 到用户的映射
  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;    // 可选字段,Go 中生成指针类型
  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()),
}
// 转换为 Go 时间
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 定义

  1. 使用 Service 关键字
  2. 使用 RPC 关键字来定义一个方法
  3. 每个方法都使用一个 message 来作为输入,以及一个 message 来作为一个输出

服务定义

service UserService {
  // 简单 RPC
  rpc GetUser(GetUserRequest) returns (User);
  
  // 服务端流式 RPC
  rpc ListUsers(ListUsersRequest) returns (stream User);
  
  // 客户端流式 RPC  
  rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);
  
  // 双向流式 RPC
  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)
    
    // 启动 goroutine 处理客户端消息
    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{})
    
    // 发送消息的 goroutine
    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
# --proto_path=: 指定.proto 文件的路径,填写。表示当前目录下
# --go_out=: 输出目录,指定 plugins=grpc 表示生成 grpc 代码
# --go_opt: 用于设置 Go 编译选项
# --grpc_out: 指定 gRPC 代码生成输出目录
# --plugin: 指定代码生成插件

# 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"
// 序列化为 JSON
jsonData, err := protojson.Marshal(user)
if err != nil {
    log.Fatal(err)
}
// 从 JSON 反序列化
jsonUser := &pb.User{}
err = protojson.Unmarshal(jsonData, jsonUser)
if err != nil {
    log.Fatal(err)
}

# 最佳实践

# 1. 字段编号规划

message User {
  // 1-15: 经常使用的字段(1 字节编码)
  int64 id = 1;
  string name = 2;
  string email = 3;
  
  // 16 及以上:不常用字段(2 字节编码)
  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

# 使用 protoc 解码二进制数据
echo "binary_data" | protoc --decode=User user.proto
# 或在 Go 中转换为 JSON
jsonData, _ := protojson.Marshal(protoMessage)
fmt.Println(string(jsonData))

# 7. 记住要点

  1. 🔢 字段编号一旦分配不要更改
  2. 🎯 1-15 编号给常用字段(节省字节)
  3. 🛡️ 删除字段要用 reserved 保留编号
  4. 📦 利用 oneof 节省内存和带宽
  5. ⏰ 时间字段使用 google.protobuf.Timestamp

# 扩展阅读

  • Protocol Buffers 官方文档
  • gRPC Go 快速开始
  • Protocol Buffers 语言指南
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

ZJM 微信支付

微信支付

ZJM 支付宝

支付宝