feature — 功能开关

pkg/core/feature 是进程内的功能开关:按名字读一个布尔值, 就能按部署环境切换代码路径,不改代码也不重新编译。这个示例注册两个开关, 暴露一个运行期翻转接口,并把取值优先级(运行时 → 环境变量 → 配置 → 默认值) 完整地跑给你看。

运行

go run ./examples/feature

curl -s localhost:8085/api/v1/checkout                              # flag 决定走 v1 还是 v2
curl -s -X POST "localhost:8085/api/v1/flags/beta-search?on=true"   # 运行期翻转
curl -s localhost:8085/api/v1/flags                                 # 所有开关的当前值

FEATURE_BETA_SEARCH=on go run ./examples/feature                    # 环境变量优先于配置

没有外部依赖,不需要 make dev。监听 :8085, -conf 默认指向 examples/feature/config.yaml; 那份配置里 new-checkout: true、beta-search: false。

它演示了什么

声明与注册

这段来自 examples/feature/main.go。两个 flag 是包级变量, 在 init 里注册——Register 只负责填入默认值, 真正的值由配置/环境/运行时决定。

// The flags this service owns, with their defaults. verticle loads the
// `feature:` section before Setup; Register only fills in the defaults.
var (
	flagNewCheckout = feature.Flag{Name: "new-checkout", Default: false}
	flagBetaSearch  = feature.Flag{Name: "beta-search", Default: false}
)

func init() {
	feature.Register(flagNewCheckout, flagBetaSearch)
}

翻转与读取

同一份文件里的两个 handler。checkout 在每个请求里读开关, 而不是在 Setup 里读一次存进结构体字段——这正是翻转能立即生效的原因。 set 展示了运行期覆盖,注释也点明了真实服务该给它加守卫。

// checkout picks its implementation per request, so a flip takes effect on
// the next call — no restart, no stale copy in a struct field.
func (s *featureService) checkout(context.Context, *http.Request) (any, error) {
	if flagNewCheckout.Enabled() {
		return map[string]string{"path": "checkout-v2", "note": "one-page flow"}, nil
	}
	return map[string]string{"path": "checkout-v1", "note": "legacy three-step flow"}, nil
}
// set is the operator switch. Real services guard it (auth middleware,
// admin-only); it exists here to show the runtime override.
func (s *featureService) set(_ context.Context, r *http.Request) (any, error) {
	name := r.PathValue("name")
	switch r.URL.Query().Get("on") {
	case "true", "1", "on":
		feature.Set(name, true)
	case "false", "0", "off":
		feature.Set(name, false)
	default:
		return nil, errs.Invalid(40001, "on must be true or false")
	}
	return map[string]any{"name": name, "enabled": feature.Enabled(name)}, nil
}

一个开关的四层来源

flowchart TD
    Q["flag.Enabled()
每个请求都调用"] --> S{"运行时
feature.Set 过吗?"} S -- 是 --> V1["用运行时值"] S -- 否 --> E{"FEATURE_<NAME>
环境变量存在且可解析吗?"} E -- 是 --> V2["用环境变量值"] E -- 否 --> C{"feature: 配置段里有吗?"} C -- 有 --> V3["用配置值"] C -- 否 --> V4["用 Register 时的 Default"] classDef win fill:#f0f7f0,stroke:#3a7a3a,color:#2b2722 class V1 win
从上到下第一个命中的来源胜出。绿色的运行时层最强,所以控制面板可以 在进程存活期间任意覆盖发布时定下的值;重启后回到环境变量/配置。

环境变量名的规则是 FEATURE_<NAME>:名字转大写、 非字母数字字符换成 _,所以 beta-search 对应 FEATURE_BETA_SEARCH。它接受 1/true/on/yes 与 0/false/off/no;无法解析的值会被忽略,回落到下一层。

验证

# 1. 配置里 new-checkout: true → 走 v2
curl -s localhost:8085/api/v1/checkout
# {"code":0,"msg":"ok","data":{"note":"one-page flow","path":"checkout-v2"}}

# 2. 所有开关的当前值
curl -s localhost:8085/api/v1/flags
# {"code":0,"msg":"ok","data":{"beta-search":false,"new-checkout":true}}

# 3. 运行期翻转 beta-search
curl -s -X POST "localhost:8085/api/v1/flags/beta-search?on=true"
# {"code":0,"msg":"ok","data":{"enabled":true,"name":"beta-search"}}
curl -s localhost:8085/api/v1/flags
# {"code":0,"msg":"ok","data":{"beta-search":true,"new-checkout":true}}

# 4. 非法值被拒
curl -i -X POST "localhost:8085/api/v1/flags/x?on=maybe"
# 400 {"code":40001,"msg":"on must be true or false","data":null}

环境变量优先于配置,用一个进程级的观察最直观:

FEATURE_NEW_CHECKOUT=off go run ./examples/feature
curl -s localhost:8085/api/v1/checkout
# {"code":0,"msg":"ok","data":{"note":"legacy three-step flow","path":"checkout-v1"}}
# 配置里写的是 true,但环境变量赢了

测试

测试里用 feature.Configure(map[string]bool{…}) 隔离取值 (它会整体替换配置层),断言的是:

go test ./examples/feature

相关

开关与审计概念页 功能开关与审计日志。 matex-setup-feature 让 AI 按规范把功能开关接进你的项目。 示例索引 全部 20 个可运行示例。

相关示例