Feature gate 的核心目標是在外部能力、部署環境或版本不同時,讓服務保留可預期行為。它明確管理功能何時啟用、關閉時如何降級、錯誤時如何回報。

本章目標

學完本章後,你將能夠:

  1. 用 config struct 集中載入 feature gate
  2. 把外部版本偵測轉成 capability
  3. 為 gate 關閉時定義降級、回錯或延後處理策略
  4. 避免在程式各處直接讀環境變數
  5. 同時測試 feature 開與關兩條路徑

【觀察】新功能上線需要可控行為

Feature gate 的核心需求來自生產環境差異。新功能可能只在部分部署環境可用,外部依賴可能版本不同,某些診斷入口只應在內網啟用,某些即時能力需要先灰度。

沒有 gate 時常見問題:

  • 新功能只能一次性全開或全關。
  • 部署環境不支援時服務直接失敗。
  • 測試只能覆蓋預設路徑。
  • 問題發生時無法快速降級。
  • 程式各處用環境變數判斷,行為難以推理。

Feature gate 的目的是讓行為決策集中、可測、可回滾。

【判讀】feature gate 是行為合約

Feature gate 的核心語意是控制某段行為是否啟用,以及未啟用時系統要做什麼。它不只是 if,而是一個操作合約。

1type Features struct {
2    RealtimePush bool
3    Diagnostics  bool
4    Pprof        bool
5}

開關名稱應描述功能,而不是描述臨時任務。RealtimePushNewCode 更能長期維護;DiagnosticsDebugStuff 更清楚。

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;如果你要先回看語言教材,可以讀:

小結

Feature gate 是生產操作工具,也是程式設計邊界。好的 gate 會集中載入、轉成 capability、定義降級策略、輸出穩定 reason,並同時測試開與關兩條路徑。它控制的是行為合約,不只是把新程式碼藏在 if 後面。