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。
它演示了什么
- 声明与注册:
feature.Flag{Name, Default}加feature.Register(…),在init()里做。 - 每个请求处读:
flag.Enabled()写在 handler 里,翻转后下一次请求就生效。 - 四层取值优先级:运行时
feature.Set→FEATURE_<NAME>→feature:配置段 → 注册时的默认值。 - 运行期翻转接口:
POST /api/v1/flags/{name}?on=true|false,非法值返回 400。 - 配置由框架灌入:
verticle.Run会在Setup之前加载feature:段。 - 未注册的名字也能开:
Enabled(name)对没注册过的开关返回 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{…}) 隔离取值
(它会整体替换配置层),断言的是:
TestConfigSelectsThePath:new-checkout为 true 时/checkout返回checkout-v2,为 false 时返回checkout-v1。TestRuntimeFlipTakesEffectOnTheNextRequest:翻转前/flags里是"beta-search":false, 调一次POST /api/v1/flags/beta-search?on=true后立刻变成"beta-search":true——下一次请求就生效,不用重启。TestBadFlipValueIsRejected:?on=maybe返回 400。
go test ./examples/feature
相关
相关示例
- examples/config:
feature:段背后那套 tag 与展开规则。 - examples/auth:翻转接口在生产上就该挂上这层守卫。
- examples/lifecycle:另一种「不改代码改变行为」的机制——配置热更。