6.4 版本偵測與 feature gate
Feature gate 的核心目標是在外部能力、部署環境或版本不同時,讓服務保留可預期行為。它明確管理功能何時啟用、關閉時如何降級、錯誤時如何回報。
本章目標
學完本章後,你將能夠:
- 用 config struct 集中載入 feature gate
- 把外部版本偵測轉成 capability
- 為 gate 關閉時定義降級、回錯或延後處理策略
- 避免在程式各處直接讀環境變數
- 同時測試 feature 開與關兩條路徑
【觀察】新功能上線需要可控行為
Feature gate 的核心需求來自生產環境差異。新功能可能只在部分部署環境可用,外部依賴可能版本不同,某些診斷入口只應在內網啟用,某些即時能力需要先灰度。
沒有 gate 時常見問題:
- 新功能只能一次性全開或全關。
- 部署環境不支援時服務直接失敗。
- 測試只能覆蓋預設路徑。
- 問題發生時無法快速降級。
- 程式各處用環境變數判斷,行為難以推理。
Feature gate 的目的是讓行為決策集中、可測、可回滾。
【判讀】feature gate 是行為合約
Feature gate 的核心語意是控制某段行為是否啟用,以及未啟用時系統要做什麼。它不只是 if,而是一個操作合約。
1type Features struct {
2 RealtimePush bool
3 Diagnostics bool
4 Pprof bool
5}開關名稱應描述功能,而不是描述臨時任務。RealtimePush 比 NewCode 更能長期維護;Diagnostics 比 DebugStuff 更清楚。
Gate 應在應用啟動時集中載入,再傳給需要的元件。不要在程式各處反覆直接讀環境變數,否則測試與推理都會變困難。
【執行】集中載入 feature config
Feature config 的核心責任是把環境變數、設定檔或啟動參數轉成明確資料。
1func LoadFeaturesFromEnv() Features {
2 return Features{
3 RealtimePush: os.Getenv("FEATURE_REALTIME_PUSH") == "1",
4 Diagnostics: os.Getenv("APP_DIAGNOSTICS") == "1",
5 Pprof: os.Getenv("APP_PPROF") == "1",
6 }
7}組裝時傳入元件:
1func main() {
2 features := LoadFeaturesFromEnv()
3
4 mux := http.NewServeMux()
5 RegisterDiagnostics(mux, features.Diagnostics)
6
7 publisher := NewPublisher(PublisherConfig{
8 RealtimeEnabled: features.RealtimePush,
9 })
10
11 _ = publisher
12}這樣功能測試可以直接建構 Features,不必依賴全域環境變數。環境變數解析只需要在 LoadFeaturesFromEnv 的測試中覆蓋。
【判讀】版本偵測要轉成能力
版本偵測的核心原則是不要讓整個程式到處比較版本字串。應把外部版本轉成 capability,內部只判斷能力。
1type Capabilities struct {
2 SupportsStreaming bool
3 SupportsMetadata bool
4}
5
6func DetectCapabilities(version semver.Version) Capabilities {
7 return Capabilities{
8 SupportsStreaming: version.GTE(semver.MustParse("2.0.0")),
9 SupportsMetadata: version.GTE(semver.MustParse("2.1.0")),
10 }
11}內部程式應寫成:
1if caps.SupportsStreaming {
2 return useStreaming(ctx)
3}
4
5return usePolling(ctx)這比到處寫 if version >= ... 更清楚,也更容易測試。版本字串是外部事實,capability 是內部行為判斷。
【策略】gate 關閉時要有降級策略
Feature gate 的核心問題是關閉時要做什麼。常見策略包括降級、回錯、隱藏入口、排程稍後處理。
| 策略 | 行為 | 適用情境 |
|---|---|---|
| fallback | 使用舊流程 | 新能力只是效率改善 |
| reject | 回明確錯誤 | 功能沒有安全替代方案 |
| hide | 不註冊 endpoint 或不顯示入口 | 使用者不應看到該功能 |
| store for later | 先保存,稍後處理 | 即時能力暫不可用但資料不能丟 |
例如即時推送關閉時,可以改成保存待處理資料:
1func (p Publisher) Publish(ctx context.Context, event DomainEvent) error {
2 if p.realtimeEnabled {
3 return p.realtime.Publish(ctx, event)
4 }
5
6 return p.repository.SaveForLater(ctx, event)
7}降級策略要符合資料語意。不能即時送出不代表可以直接丟掉重要事件。
【執行】HTTP endpoint 可用 gate 控制註冊或行為
HTTP feature gate 的核心選擇是「不註冊 endpoint」或「註冊但回明確錯誤」。兩者語意不同。
不註冊 endpoint:
1if features.Diagnostics {
2 RegisterDiagnostics(mux, true)
3}適合診斷入口、內部工具或不希望使用者看見的功能。
註冊但回錯:
1func HandleRealtimeExport(features Features) http.HandlerFunc {
2 return func(w http.ResponseWriter, r *http.Request) {
3 if !features.RealtimePush {
4 http.Error(w, "realtime export is disabled", http.StatusNotImplemented)
5 return
6 }
7
8 startRealtimeExport(w, r)
9 }
10}適合公開 API,讓呼叫端知道功能存在但目前不可用。
【策略】gate 不應散落成巢狀 if
Feature gate 的核心維護風險是判斷散落在多層呼叫中,最後沒人知道功能到底何時啟用。
反模式:
1if os.Getenv("FEATURE_REALTIME_PUSH") == "1" {
2 if version >= "2.0.0" {
3 if user.Enabled {
4 // ...
5 }
6 }
7}較清楚的做法是先組出 decision:
1type RealtimeDecision struct {
2 Enabled bool
3 Reason string
4}
5
6func DecideRealtime(features Features, caps Capabilities) RealtimeDecision {
7 if !features.RealtimePush {
8 return RealtimeDecision{Enabled: false, Reason: "feature_disabled"}
9 }
10 if !caps.SupportsStreaming {
11 return RealtimeDecision{Enabled: false, Reason: "streaming_not_supported"}
12 }
13 return RealtimeDecision{Enabled: true}
14}Decision 物件讓 log、測試與錯誤回應都能使用相同 reason。
【執行】log 要記錄 gate decision
Feature gate 的核心操作需求是知道功能為何啟用或關閉。當 gate 影響行為時,應記錄穩定 reason。
1decision := DecideRealtime(features, caps)
2logger.Info("realtime decision",
3 "feature", "realtime_push",
4 "enabled", decision.Enabled,
5 "reason", decision.Reason,
6)這能回答「功能為什麼沒有走即時推送」這類問題。Reason 應是小集合,不要塞完整錯誤字串。
【測試】開與關兩條路徑都要測
Feature gate 測試的核心規則是同時測啟用與停用路徑。只測預設值很容易讓另一條路徑壞掉。
停用路徑:
1func TestHandleRealtimeExportFeatureDisabled(t *testing.T) {
2 req := httptest.NewRequest(http.MethodPost, "/export", nil)
3 rec := httptest.NewRecorder()
4
5 handler := HandleRealtimeExport(Features{RealtimePush: false})
6 handler.ServeHTTP(rec, req)
7
8 if rec.Code != http.StatusNotImplemented {
9 t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotImplemented)
10 }
11}啟用路徑:
1func TestDecideRealtimeEnabled(t *testing.T) {
2 decision := DecideRealtime(
3 Features{RealtimePush: true},
4 Capabilities{SupportsStreaming: true},
5 )
6
7 if !decision.Enabled {
8 t.Fatalf("realtime should be enabled, reason %q", decision.Reason)
9 }
10}環境變數解析應單獨測 LoadFeaturesFromEnv。功能測試應直接傳入 Features,不要依賴全域環境狀態。
本章不處理
本章先處理服務內部的 gate 行為邊界;遠端 feature flag 平台與灰度流程,會在下列章節再往外延伸:
和 Go 教材的關係
這一章承接的是 composition root、handler boundary 與 runtime gate;如果你要先回看語言教材,可以讀:
- Go:composition root 與依賴組裝
- Go:把 handler 邏輯拆成可測單元
- Go:用 interface 隔離外部依賴
- Go:testing 基礎
- Go 進階:Kubernetes、systemd 與 load balancer 合約
小結
Feature gate 是生產操作工具,也是程式設計邊界。好的 gate 會集中載入、轉成 capability、定義降級策略、輸出穩定 reason,並同時測試開與關兩條路徑。它控制的是行為合約,不只是把新程式碼藏在 if 後面。