Proto 結構重構:分離 client-facing common types 與 internal services¶
文檔資訊
- 分類: architecture
- 難度: intermediate
- 預估閱讀時間: 8 分鐘
- 標籤:
protobuf,buf,code-generation,csharp,go,api-design,breaking-change-prevention
摘要¶
解決 buf v2 inputs.paths 僅支援目錄級過濾的限制,將 matchrpg/v1/ 混雜目錄拆分為 matchrpg/common/v1/(客戶端共用型別)與 matchrpg/internal/v1/(伺服器內部服務),並保留 C# namespace 不變以避免前端 breaking change。
關鍵學習¶
-
buf v2 的 inputs.paths 只支援目錄級過濾,無法白名單單個 .proto 檔案,必須用目錄結構解決
-
移動 proto 時可以讓 package 跟著目錄走,但 csharp_namespace 獨立控制,可保留舊值避免前端 breaking change
-
proto 目錄結構應依「交付對象」區分,而非單純依功能分類
-
common.proto 被多個服務 import,移動時需同步更新所有 import path
-
Wire format 在 Preview 階段做 package rename 風險低,但上線後需謹慎
技術細節¶
問題根源¶
buf v2 的 buf.gen.client.yaml 中 inputs.paths 只能指定目錄,無法精確白名單單一 .proto 檔案。原本 matchrpg/v1/ 同時放了客戶端需要的 common.proto 和伺服器內部的 admin.proto、config.proto、health.proto,hotfix 是把整個目錄加白名單,導致 Admin.cs、Config.cs、Health.cs 被錯誤交付給客戶端。
解決方案:目錄重構¶
根據交付對象將 matchrpg/v1/ 拆分:
matchrpg/
├── common/v1/ # 客戶端 (加入 buf client 白名單)
│ ├── common.proto # RequestMeta, ResponseMeta, EventBatch
│ ├── errors.proto # ErrorCode, ErrorCategory
│ └── sync.proto # SyncService
├── internal/v1/ # 伺服器端 (不交付客戶端)
│ ├── admin.proto
│ ├── config.proto
│ └── health.proto
└── dungeon/v1/ # (更新 import paths)
C# Namespace 保留策略¶
移動後的 common.proto 保留原 csharp_namespace,讓 Go package 跟著目錄走但 C# 端零改動:
package matchrpg.common.v1;
option go_package = "...pkg/pb/matchrpg/common/v1;commonv1";
option csharp_namespace = "MatchRPG.Proto.V1"; // 保持不變
buf.gen.client.yaml 更新¶
inputs:
- directory: .
paths:
- matchrpg/common
- matchrpg/player
- matchrpg/teach
- matchrpg/dungeon
What Changed¶
目錄結構重構:將原本混雜的 matchrpg/v1/ 依交付對象拆成兩個目錄。common/v1/ 放客戶端共用型別(common、errors、sync),internal/v1/ 放伺服器內部服務(admin、config、health)。
Import path 更新:dungeon/v1 和所有引用 matchrpg/v1/common.proto 的 proto 檔需更新 import path;Go 代碼約 15 個檔案需更新 import package。
buf 白名單精確化:buf.gen.client.yaml 改為指向 matchrpg/common 而非舊的 matchrpg/v1,讓 CI 生成的 C# 代碼不再包含 Admin.cs、Config.cs、Health.cs。
So What¶
這個重構解決了 proto monorepo 中常見的「交付邊界不清」問題。透過目錄結構強制分離客戶端 API 和內部服務,配合 buf 的目錄級過濾,確保自動生成的 client SDK 永遠不會意外包含內部服務代碼。
保留 C# namespace 的策略是關鍵:允許伺服器端做結構性重構,同時讓前端完全無感知,降低了跨 repo 協調成本。
Trade-offs¶
- 優:buf 白名單精確,CI 自動防止內部服務代碼洩漏到客戶端 SDK
- 優:C# namespace 不變,前端零改動
- 優:Go package 語義更清晰(commonv1 vs internalpb)
- 缺:Go import 需要更新約 15 個檔案,機械式工作量較大
- 缺:package 名稱從 matchrpg.v1 改為 matchrpg.common.v1,Wire format 技術上有變
- 缺:buf breaking check CI 可能因 package rename 失敗,需在 preview branch 加臨時例外
Try It Fast¶
# Step 1: 移動 proto 檔案
mkdir -p api/proto/matchrpg/common/v1 api/proto/matchrpg/internal/v1
git mv api/proto/matchrpg/v1/common.proto api/proto/matchrpg/common/v1/
git mv api/proto/matchrpg/v1/errors.proto api/proto/matchrpg/common/v1/
git mv api/proto/matchrpg/v1/sync.proto api/proto/matchrpg/common/v1/
git mv api/proto/matchrpg/v1/admin.proto api/proto/matchrpg/internal/v1/
git mv api/proto/matchrpg/v1/config.proto api/proto/matchrpg/internal/v1/
git mv api/proto/matchrpg/v1/health.proto api/proto/matchrpg/internal/v1/
# Step 2: 重新生成代碼
make proto-clean && make proto
make proto-client
# Step 3: 驗證生成的 C# 檔案不含 internal 服務
ls generated/csharp/ # 不應有 Admin.cs, Config.cs, Health.cs
# Step 4: 編譯驗證
make wire && go build ./... && go test ./...
Recommendation¶
- 在 feature branch 進行,避免影響 main 的 buf breaking check CI
- 先更新 proto 檔案內容(package、go_package、import paths),再執行 make proto,讓編譯錯誤引導找到所有需要更新的 Go 檔案
- 用 grep -r 'matchrpg/v1' --include='*.go' 確認所有舊 import 已被替換
- 驗證 C# 生成結果時,明確檢查 generated/csharp/ 目錄不含 Admin.cs、Config.cs、Health.cs
- 如果 buf breaking check CI 失敗,在 buf.yaml 加 breaking.ignore 臨時例外,PR 合併後再移除