在前几篇文章中,我们已经掌握了 Protocol Buffers(Protobuf)的基础语法、.proto 文件的结构、以及如何使用 Go 和 Java 进行数据的序列化与反序列化操作。本篇文章将深入探讨 Protobuf 的高级特性,包括:

  1. 嵌套消息(Nested Messages)
  2. Oneof 字段(Oneof Fields)
  3. Map 类型(Map Types)
  4. 自定义选项(Custom Options)
  5. 向后兼容性设计与最佳实践

我将通过详细的代码示例分步解释,帮助你彻底理解这些功能的设计思想、使用场景以及实现细节。文章篇幅较长,内容全面,适合希望深入掌握 Protobuf 的开发者。

这篇文章并没有集成grpc,主要是为了让大家更好地理解protobuf,后面的文章都会集成grpc,集成之后生成源码的命令会有所变化(这里也给了部分提示),希望大家能注意到这些不同。


一、嵌套消息(Nested Messages)

1. 什么是嵌套消息?

嵌套消息允许在一个 .proto 文件中定义多个消息类型,并将一个消息作为另一个消息的字段。这种设计非常适合表达层级关系复合结构的数据模型。

2. 为什么需要嵌套消息?

  • 减少冗余:避免重复定义相同的数据结构。
  • 提高可读性:将复杂的数据模型拆分为逻辑清晰的子结构。
  • 支持模块化设计:方便团队协作和代码维护。

3. 示例:定义嵌套消息

syntax = "proto3";package user;option go_package = "/user;user"; // 指定生成的 Go 包路径(生成源码的路径和包名,前面是路径后面是包名,可以自己定义)
//option go_package = ".;user"; //这个可以生成在当前目录下// 定义 Address 消息
message Address {string city = 1;string street = 2;
}// 定义 UserInfo 消息,引用 Address
message UserInfo {string name = 1;int32 age = 2;Address address = 3; // 嵌套 Address 消息
}

4. Go 示例详解

(1)生成代码

运行以下命令生成 Go 代码:

protoc --go_out=. user.proto

注意:这里跟据版本不同命令可能会有变化,新版本以及安装了grpc之后可以用以下命令(后面的命令都是这样的,跟据需求自己修改即可):

protoc --go_out=. --go-grpc_out=. user.proto

(2)编写代码
package mainimport ("fmt"pb "./user_go_proto" // 根据你的路径调整"github.com/golang/protobuf/proto"
)func main() {// 创建嵌套消息 Addressaddress := &pb.Address{City:   "Shanghai",Street: "Nanjing Road",}// 创建主消息 UserInfo,引用 Addressuser := &pb.UserInfo{Name:    "Alice",Age:     25,Address: address, // 嵌套字段赋值}// 序列化为字节流data, _ := proto.Marshal(user)// 反序列化为对象newUser := &pb.UserInfo{}proto.Unmarshal(data, newUser)// 访问嵌套字段fmt.Printf("Address: %s, %s\n", newUser.GetAddress().GetCity(), newUser.GetAddress().GetStreet())
}
(3)代码解析
  • Address 消息Address 是一个独立的消息类型,包含城市和街道字段。
  • UserInfo 消息UserInfo 包含一个 Address 类型的字段,通过 address 字段引用。
  • 代码调用:通过 GetAddress() 方法访问嵌套字段,并进一步调用 GetCity() 和 GetStreet()

5. Java 示例详解

(1)生成代码

运行以下命令生成 Java 代码:

protoc --java_out=. user.proto
protoc --java_out=. --java-grpc_out=. user.proto //新版本命令,下面和这个一样,不再做提示
(2)编写代码
import user.UserInfo;
import user.Address;
import java.io.*;public class Main {public static void main(String[] args) throws IOException {// 创建嵌套消息 AddressAddress address = Address.newBuilder().setCity("Beijing").setStreet("Chang'an Avenue").build();// 创建主消息 UserInfo,引用 AddressUserInfo user = UserInfo.newBuilder().setName("Bob").setAge(30).setAddress(address) // 嵌套字段赋值.build();// 序列化为字节流byte[] data = user.toByteArray();// 反序列化为对象UserInfo newUser = UserInfo.parseFrom(data);// 访问嵌套字段System.out.println("Address: " + newUser.getAddress().getCity() + ", " + newUser.getAddress().getStreet());}
}
(3)代码解析
  • Address 消息Address 是一个独立的类,包含 city 和 street 字段。
  • UserInfo 消息UserInfo 类通过 setAddress() 方法引用 Address 对象。
  • 代码调用:通过 getAddress() 方法访问嵌套字段,并进一步调用 getCity() 和 getStreet()

二、Oneof 字段(Oneof Fields)

1. 什么是 Oneof 字段?

oneof 字段是一组字段的集合,最多只有一个字段可以被设置。它适用于互斥的场景,例如登录方式(用户名、手机号、邮箱只能选其一)。

2. 为什么需要 Oneof 字段?

  • 节省空间:只存储一个字段,避免冗余。
  • 强制互斥:确保业务逻辑中不会同时设置多个字段。
  • 简化逻辑:减少对字段是否为空的判断。

3. 示例:定义 Oneof 字段

message UserLogin {oneof login_method {string username = 1;string phone = 2;string email = 3;}string password = 4;
}

4. Go 示例详解

(1)生成代码
protoc --go_out=. user.proto
(2)编写代码
package mainimport ("fmt"pb "./user_go_proto""github.com/golang/protobuf/proto"
)func main() {// 设置 username 登录方式login := &pb.UserLogin{LoginMethod: &pb.UserLogin_Username{"alice123"},Password:    "pass123456",}// 序列化为字节流data, _ := proto.Marshal(login)// 反序列化为对象newLogin := &pb.UserLogin{}proto.Unmarshal(data, newLogin)// 判断并访问 oneof 字段switch v := newLogin.LoginMethod.(type) {case *pb.UserLogin_Username:fmt.Println("Logged in by username:", v.Username)case *pb.UserLogin_Phone:fmt.Println("Logged in by phone:", v.Phone)case *pb.UserLogin_Email:fmt.Println("Logged in by email:", v.Email)default:fmt.Println("Unknown login method")}
}
(3)代码解析
  • oneof 字段类型LoginMethod 是一个联合类型(interface{}),需要通过类型断言访问具体字段。
  • 设置字段:通过 &pb.UserLogin_Username{} 设置 username 字段。
  • 访问字段:使用 switch 语句判断具体字段类型,并提取值。

5. Java 示例详解

(1)生成代码
protoc --java_out=. user.proto
(2)编写代码
import user.UserLogin;
import java.io.*;public class Main {public static void main(String[] args) throws IOException {// 设置 email 登录方式UserLogin login = UserLogin.newBuilder().setEmail("alice@example.com").setPassword("pass123456").build();// 序列化为字节流byte[] data = login.toByteArray();// 反序列化为对象UserLogin newLogin = UserLogin.parseFrom(data);// 判断并访问 oneof 字段if (newLogin.hasUsername()) {System.out.println("Logged in by username: " + newLogin.getUsername());} else if (newLogin.hasPhone()) {System.out.println("Logged in by phone: " + newLogin.getPhone());} else if (newLogin.hasEmail()) {System.out.println("Logged in by email: " + newLogin.getEmail());} else {System.out.println("Unknown login method");}}
}
(3)代码解析
  • oneof 字段类型UserLogin 类提供 hasXxx() 方法判断字段是否存在。
  • 设置字段:通过 setEmail() 等方法设置具体字段。
  • 访问字段:通过 getEmail() 等方法提取值。

三、Map 类型(Map Types)

1. 什么是 Map 类型?

Map 是 Proto3 中支持的一种键值对结构,类似于 map[string]stringDictionary<string, string>。它非常适合表达元数据、配置信息等。

2. 为什么需要 Map 类型?

  • 灵活存储键值对:无需预先定义所有键。
  • 简化代码:避免手动管理多个字段。
  • 支持动态数据:适用于不确定键值对数量的场景。

3. 示例:定义 Map 类型

message UserProfile {map<string, string> metadata = 1; // 键值对类型
}

4. Go 示例详解

(1)生成代码
protoc --go_out=. user.proto
(2)编写代码
package mainimport ("fmt"pb "./user_go_proto""github.com/golang/protobuf/proto"
)func main() {// 创建 map 并赋值profile := &pb.UserProfile{Metadata: map[string]string{"role":       "admin","department": "IT",},}// 序列化为字节流data, _ := proto.Marshal(profile)// 反序列化为对象newProfile := &pb.UserProfile{}proto.Unmarshal(data, newProfile)// 遍历 mapfor k, v := range newProfile.Metadata {fmt.Printf("%s: %s\n", k, v)}
}
(3)代码解析
  • map 类型Metadata 是一个 map[string]string 类型。
  • 赋值:直接通过 Go 的 map 语法初始化。
  • 遍历:通过 range 遍历键值对。

5. Java 示例详解

(1)生成代码
protoc --java_out=. user.proto
(2)编写代码
import user.UserProfile;
import java.io.*;public class Main {public static void main(String[] args) throws IOException {// 创建 map 并赋值UserProfile profile = UserProfile.newBuilder().putMetadata("theme", "dark").putMetadata("lang", "zh-CN").build();// 序列化为字节流byte[] data = profile.toByteArray();// 反序列化为对象UserProfile newProfile = UserProfile.parseFrom(data);// 遍历 mapnewProfile.getMetadataMap().forEach((key, value) -> {System.out.println(key + ": " + value);});}
}
(3)代码解析
  • map 类型metadata 是一个 Map<String, String> 类型。
  • 赋值:通过 putMetadata() 方法添加键值对。
  • 遍历:通过 getMetadataMap() 获取 map,并使用 forEach() 遍历。

四、自定义选项(Custom Options)

1. 什么是自定义选项?

自定义选项允许你在 .proto 文件中添加元信息,用于描述字段、消息或服务的额外属性。这些信息可以被编译器或插件读取,用于生成文档、校验逻辑等。

2. 为什么需要自定义选项?

  • 添加业务规则:例如字段的校验规则。
  • 扩展编译器行为:通过插件生成特定代码。
  • 提高可读性:通过注释描述字段的用途。

3. 示例:定义自定义选项

import "google/protobuf/descriptor.proto";// 定义新的选项类型
extend google.protobuf.FieldOptions {string validation_rule = 50001;
}// 使用自定义选项
message User {string email = 1 [(validation_rule) = "email"];
}

4. 代码解析

  • 定义选项:通过 extend 扩展 google.protobuf.FieldOptions,添加 validation_rule 字段。
  • 使用选项:在字段定义中使用 [(validation_rule) = "email"] 添加元信息。

⚠️ 注意:自定义选项需要配合插件使用,否则无法生效。这属于高级用法,通常用于生成文档或校验逻辑。


五、向后兼容性设计与最佳实践

1. 什么是向后兼容性?

向后兼容性是指新版本的协议能够兼容旧版本的客户端。Protobuf 的设计目标之一就是支持良好的向后兼容性。

2. 为什么需要向后兼容性?

  • 平滑升级:在不中断服务的情况下更新数据格式。
  • 减少维护成本:避免因版本升级导致的代码重构。
  • 支持多版本共存:允许不同版本的客户端和服务端同时运行。

3. 向后兼容性设计原则

操作是否允许说明
新增字段✅ 允许使用新的字段编号
删除字段❌ 不允许会导致旧客户端解析失败
修改字段类型❌ 不允许会导致序列化失败
修改字段编号❌ 不允许会导致解析失败
修改字段名✅ 允许只影响生成代码,不影响数据格式

4. 最佳实践

  • 字段编号递增:新增字段时,使用更大的编号。
  • 避免删除字段:如果字段不再使用,标记为 deprecated
  • 使用 repeated 替代数组repeated 字段支持动态添加元素。
  • 版本控制:在 .proto 文件中添加版本注释,例如:
    // Version 1.0.0
    message User {string name = 1;
    }

六、总结

在本文中,我们详细讲解了 Protobuf 的几个关键高级特性:

  1. 嵌套消息:通过层级结构组织复杂数据。
  2. Oneof 字段:实现互斥字段的逻辑控制。
  3. Map 类型:高效处理键值对数据。
  4. 自定义选项:扩展协议的元信息。
  5. 向后兼容性设计:确保版本升级的平滑过渡。

这些功能使得 Protobuf 在构建大型系统和服务接口时具备极高的灵活性和可扩展性。通过 Go 和 Java 的详细示例,我们展示了如何在实际开发中应用这些特性,并提供了分步解析和代码注释,帮助你深入理解每一步操作。


七、下期预告

在下一篇文章中,我们将继续深入 Protobuf 的高级应用,包括:

  • gRPC 服务定义与 Protobuf 的集成
  • 如何在 gRPC 中使用流式通信
  • 多语言服务间交互的最佳实践

 建议收藏本文作为日常开发参考手册!

如果你正在开发高性能服务、微服务架构、分布式系统,Protobuf 的这些高级特性将是你不可或缺的工具。希望这篇文章能帮助你更自信地在项目中使用 Protobuf,并享受它带来的效率提升和开发体验优化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/910238.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/910238.shtml
英文地址,请注明出处:http://en.pswp.cn/news/910238.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

golang--数据类型与存储

在 Go 语言中&#xff0c;理解值类型&#xff08;value types&#xff09;和引用类型&#xff08;reference types&#xff09;的区别对于编写高效、正确的代码至关重要。以下是主要的区别点和需要注意的特殊情况&#xff1a; 一、值类型&#xff08;Value Types&#xff09; …

uniapp——轮播图、产品列表轮播、上一页、下一页、一屏三张图

案例展示 组件封装 <template><view><view class="showSwiperBox"><view class="topSwiper"><swiper class="swiper" :autoplay="autoplay" interval="5000" :previous-margin="margin&qu…

用Python实现安全封装EXE文件加密保护工具

一、概述 这个Python脚本实现了一个强大的EXE文件加密保护工具,它能够将任何Windows可执行文件封装到一个带密码保护的GUI程序中。核心功能包括: 使用AES-256加密算法保护原始EXE文件 创建美观的密码验证界面 支持自定义程序图标 自动处理PyInstaller打包过程 修复Tkinter在…

vue3监听属性watch和watchEffect的详解

文章目录 1. 前言2. 常规用法3. 监听对象和route变化4. 使用场景4.1 即时表单验证4.2 搜索联想功能4.3 数据变化联动处理 5. watchEffect详解5-1 基本概念5-2 核心用法基础示例&#xff1a;自动响应依赖变化处理异步副作用停止监听与清理副作用 5-3 高级场景应用监听多个响应式…

Spring IoC核心实现揭秘

Spring IoC(控制反转)的实现机制是Spring框架的核心,其本质是将对象的创建、依赖管理和生命周期控制权从应用程序代码转移到容器中。以下是其核心实现机制: 🔧 一、核心实现步骤 配置元数据加载 容器启动时读取XML/注解/Java配置类,解析为BeanDefinition对象(包含类名、…

Solidity内部合约创建全解析:解锁Web3开发新姿势

合约创建基础 new 关键字创建合约 在 Solidity 中&#xff0c;new关键字是创建合约实例的最基本方式&#xff0c;它就像是一个 “魔法钥匙”&#xff0c;能够在以太坊区块链上生成一个全新的合约实例。使用new关键字创建合约的过程非常直观&#xff0c;就像我们在其他编程语言…

OCR大模型,破解金融文档处理困境,从文字识别到文字理解

金融机构在日常运营中处理海量文档。这些文档类型多样&#xff0c;格式复杂&#xff0c;是业务运营的基础。如何高效、准确地处理这些文档&#xff0c;直接影响机构的运营效率与风险控制水平。新一代的OCR大模型技术为此提供了有效的解决方案。它提升了文档处理的自动化程度与数…

2025.6.21笔记(2)

1.编写一个程序&#xff0c;输入一个整数&#xff0c;判断它是奇数还是偶数 解题思路&#xff1a; 1.因为要判断输入的数是奇数还是偶数&#xff0c;所以要用到if判断 2.判读奇偶数&#xff1a;如果这个数%20&#xff0c;则它为偶数&#xff0c;如果这个数%2!0&#xff0c;则…

【Ambari3.0.0 部署】Step7—Mariadb初始化-适用于el8

如果有其他系统部署需求可以参考原文 https://doc.janettr.com/install/manual/ MariaDB 10 是 Ambari 及大数据平台的常见数据库方案。本文适配 Rocky Linux 8.10&#xff0c;涵盖 MariaDB 10.11 推荐安装、YUM 源配置、参数优化、初始化和安全设置&#xff0c;帮助你一步到位…

SpringBoot电脑商城项目--删除收获地址+热销排行

删除收获地址 1 删除收获地址-持久层 1.1 规划sql语句 在删除操作之前判断该数据是否存在&#xff0c;判断该条地址的归属是否是当前的用户执行删除收货地址的操作 delete from t_address where aid? 如果用户删除的时默认地址&#xff0c;将剩下地址的某一条作为默认收货地…

MIMIC-III 数据集文件简介

文件简介&#xff1a; 共26个文件 admissions.csv 患者入院信息&#xff08;入院时间、出院时间、入院类型、科室等&#xff09;。 callout.csv ICU 外科室请求 ICU 会诊的呼叫记录。 caregivers.csv 护理患者的医护人员信息&#xff08;身份、角色等&#xff09;。…

UL/CE双认证!光宝MOC3052-A双向可控硅输出光耦 智能家居/工业控制必备!

光宝MOC3052-A双向可控硅输出光耦详解 1. 产品定位 MOC3052-A 是光宝科技&#xff08;Lite-On&#xff09;推出的 双向可控硅驱动光耦&#xff0c;属于光电隔离型半导体器件&#xff0c;主要用于交流负载的隔离控制&#xff0c;实现低压控制电路&#xff08;如MCU&#xff09;…

让没有小窗播放的视频网站的视频小窗播放

让没有小窗播放的视频网站的视频小窗播放 // 视频小窗播放控制台脚本 // 将此代码复制到浏览器控制台运行 // 运行后&#xff0c;页面中的视频将添加小窗播放功能(function() {// 获取页面中的所有video元素const videos document.querySelectorAll(video);if (videos.length…

Linux内核在启动过程中挂载根文件系统rootfs的过程

一、挂载根文件系统rootfs的过程&#xff1a; 1. ‌初始虚拟根文件系统的挂载‌ 内核启动时首先会创建并挂载一个‌临时虚拟根文件系统&#xff08;如initramfs或rootfs&#xff09;‌‌15。该阶段主要作用&#xff1a; 提供基础的设备节点和目录结构&#xff0c;确保内核能访…

【LeetCode】力扣题——轮转数组、消失的数字、数组串联

&#x1f525;个人主页&#xff1a;艾莉丝努力练剑 ❄专栏传送门&#xff1a;《C语言》、《数据结构与算法》、C语言刷题12天IO强训 &#x1f349;学习方向&#xff1a;C/C方向 ⭐️人生格言&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xff0c;…

Java Stream详解

Java Stream详解 Stream 是 Java 8 引入的流式数据处理工具&#xff0c;可以像流水线一样对集合数据进行高效操作&#xff08;过滤、转换、统计等&#xff09;。核心特点&#xff1a; 链式操作&#xff1a;支持多个操作串联不修改原始数据&#xff1a;生成新结果支持并行处理…

Java回归循环理解

一、Java循环的四种 1. 传统for循环 - 精确控制的首选 // 遍历数组 int[] numbers {1, 2, 3, 4, 5}; for (int i 0; i < numbers.length; i) {System.out.println(numbers[i]); }// 嵌套示例&#xff1a;矩阵遍历 int[][] matrix {{1, 2}, {3, 4}}; for (int row 0; r…

飞腾D2000金融工控主板,点亮经济高质量发展

近年来&#xff0c;国家不断推出金融行业的政策和法规&#xff0c;推动金融业高质量发展。在国家大力推进金融行业改革和创新的大环境下&#xff0c;金融工控主板市场也迎来了新的发展机遇。随着国产CPU技术的不断突破&#xff0c;以及我国对金融安全重视程度的提高&#xff0c…

SimpleITK——创建nrrd体素模型

在介绍如何生成nrrd前&#xff0c;了解一下为什么医学影像上一般使用nrrd的体素模型&#xff1f; 为什么医学影像上一般使用nrrd的体素模型&#xff1f; 在医学影像领域&#xff0c;‌NRRD&#xff08;Nearly Raw Raster Data&#xff09;格式‌被广泛用于存储体素模型&#x…

Docker容器部署KES

一、安装部署 1&#xff0c;导入镜像 #导入镜像&#xff08;root用户&#xff09; [rootnode docker ]# mv kdb_x86_64_V008R006C009B0014.tar kingbase.tar [rootnode docker]# docker load -i kingbase.tar#查看镜像&#xff08;root用户&#xff09; [rootnode docker]# d…