Protobuf 高級特性詳解 —— 嵌套消息、Oneof 字段與自定義選項

在前幾篇文章中,我們已經掌握了 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…