跳轉到

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

  1. 在 feature branch 進行,避免影響 main 的 buf breaking check CI
  2. 先更新 proto 檔案內容(package、go_package、import paths),再執行 make proto,讓編譯錯誤引導找到所有需要更新的 Go 檔案
  3. 用 grep -r 'matchrpg/v1' --include='*.go' 確認所有舊 import 已被替換
  4. 驗證 C# 生成結果時,明確檢查 generated/csharp/ 目錄不含 Admin.cs、Config.cs、Health.cs
  5. 如果 buf breaking check CI 失敗,在 buf.yaml 加 breaking.ignore 臨時例外,PR 合併後再移除

本文檔由 Semi-Brain 自動生成

Session ID: ce632061-4850-4646-b2a2-cd5752d01b26

分析信心度: 72%