Getting started¶
Ten minutes, from an empty main to a tool that gates one command on a feature and
evaluates one dynamic flag with the manifest as its fallback.
1. A descriptor¶
A feature is anything with an identity, a kind and a default. The core asks four questions of it and nothing more, so it is an interface; carry whatever else you need on your own type.
package feat
import "gitlab.com/phpboyscout/go/features"
type descriptor struct {
id features.ID
kind features.Kind
on bool
dynamic bool
}
func (d descriptor) FeatureID() features.ID { return d.id }
func (d descriptor) FeatureKind() features.Kind { return d.kind }
func (d descriptor) DefaultOn() bool { return d.on }
func (d descriptor) IsDynamic() bool { return d.dynamic }
const (
Reports features.ID = "reports"
BetaRanking features.ID = "beta-ranking"
)
func init() {
r := features.Default()
features.MustDeclare(r, descriptor{id: Reports, kind: "command", on: true})
features.MustDeclare(r, descriptor{id: BetaRanking, kind: "experiment", dynamic: true})
}
MustDeclare panics on a duplicate or an incomplete descriptor. That is deliberate: this
runs at init, where there is no caller to return an error to, and a silently dropped
feature would surface much later somewhere confusing. It is the only panic in the package.
2. A contribution¶
A package that owns a feature can also contribute what the feature brings, under a Slot
name of your choosing. The core knows a slot is a name and a value; you define the type.
const SlotCommand features.Slot = "command"
func init() {
features.Default().Contribute(Reports, SlotCommand, func() string { return "reports" })
}
3. Resolve, once¶
When your tool builds itself, take a snapshot and resolve the set from the tool's own
choices. The states come from wherever you keep them (a manifest, a config file, flags);
Enable and Disable build them.
snapshot := features.Default().Snapshot()
set, err := features.Resolve(snapshot, features.Apply(nil,
features.Disable(feat.Reports),
))
if err != nil {
// Enabling an ID nothing declared is the one error: fix the wiring.
log.Fatal(err)
}
The snapshot is immutable. A feature declared after it is not in it, and nothing panics;
a fresh snapshot sees it. Disabling an ID nothing declared is not an error: it is kept on
set.Ignored() so a diagnostic can report it.
4. Gate something static¶
if set.Enabled(feat.Reports) {
for _, c := range set.Contributions(feat.Reports, feat.SlotCommand) {
register(c.(func() string)())
}
}
Contributions on a Set hands out only the contributions of enabled features, and
ContributionsOf[T] does the type assertion for you and names the slot and feature when one
does not fit.
5. Evaluate a dynamic flag¶
BetaRanking reported IsDynamic, so a Backend may override it at request time. With no
backend yet, the Set itself is an Evaluator and answers statically:
dec, _ := set.Evaluate(ctx, feat.BetaRanking, features.EvalContext{})
// dec.Enabled == false, dec.Reason == features.ReasonStatic
When a backend arrives, wrap it in Dynamic. Nothing at the call site changes:
flags := features.Dynamic(set, backend, features.WithInitTimeout(2*time.Second))
_ = flags.Init(ctx)
dec, err := flags.Evaluate(ctx, feat.BetaRanking, features.EvalContext{TargetingKey: userID})
If the backend is not ready, errors, or does not know the flag, dec carries the static
state with ReasonFallback and err says why. Reports is static-only, so the backend is
never consulted for it, whatever it says.
That is the whole shape. The how-to guides cover each role in detail.