Use the Featurevisor Go SDK to evaluate flags, variations and variables in Go applications.
See the Go example application.
- Installation
- Initialization
- Context
- Check if enabled
- Getting variation
- Getting variables
- Conditions
- Composition
- Getting evaluations
- Sticky
- Setting datafile
- Diagnostics
- Events
- Evaluation details
- Modules
- Child instance
- Close
- OpenFeature
- CLI usage
- GitHub repositories
- Development of this package
- License
Upgrading an existing application? Follow the Go migration guide.
In your Go application, install the SDK using Go modules:
go get github.com/featurevisor/featurevisor-go/v4The SDK can be initialized by passing datafile content directly:
package main
import (
"fmt"
"io"
"net/http"
"github.com/featurevisor/featurevisor-go/v4"
)
func main() {
datafileURL := "https://cdn.yoursite.com/featurevisor-all.json"
resp, err := http.Get(datafileURL)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Errorf("datafile request failed: %s", resp.Status))
}
datafileBytes, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
var datafileContent featurevisor.DatafileContent
if err := datafileContent.FromJSON(string(datafileBytes)); err != nil {
panic(err)
}
f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Datafile: datafileContent,
})
defer f.Close()
fmt.Println(f.IsEnabled("checkout", featurevisor.Context{"userId": "123"}))
}The main runtime API is featurevisor.CreateFeaturevisor():
f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Datafile: datafileContent,
})Most applications only need CreateFeaturevisor, the Featurevisor instance type, and FeaturevisorOptions. Public extension and observability types include FeaturevisorModule, FeaturevisorDiagnostic, and the datafile model types.
Concurrent evaluations are safe after an instance is configured. Do not call state-changing methods such as SetDatafile, SetContext, SetStickyFeatures, SetStickyVariables, AddModule, RemoveModule, or Close concurrently with evaluations or with each other. Apply those changes from a serialized update path. Module, event, and diagnostic callbacks must synchronize mutable state that they capture.
Featurevisor evaluates four kinds of values:
- Flag (
bool): whether the feature is enabled or not - Variation (
string): the variation of the feature (if any) - Feature variable: a configuration value owned by a feature
- Global variable: a configuration value evaluated independently of a feature
These evaluations are run against the provided context.
Contexts are attribute values you pass to the SDK for evaluating features against.
Conditions compare these attributes using the operators, including empty and notEmpty.
Date comparisons accept portable timestamp strings and time.Time. Offsets must stay within 14 hours and both local and UTC years within 0001 to 9999. Fractions are truncated to milliseconds. The zero time.Time{} is a valid year 0001 instant, not a missing date. Omit the context entry or use a nil pointer for an unknown date.
They are plain maps:
context := featurevisor.Context{
"userId": "123",
"country": "nl",
// ...other attributes
}Set context when you create the instance:
import (
"github.com/featurevisor/featurevisor-go/v4"
)
f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Context: featurevisor.Context{
"deviceId": "123",
"country": "nl",
},
})This is useful for values that don't change too frequently and available at the time of application startup.
Add context later:
f.SetContext(featurevisor.Context{
"userId": "234",
})This will merge the new context with the existing one (if already set).
Replace the context by passing true as the second argument:
f.SetContext(featurevisor.Context{
"deviceId": "123",
"userId": "234",
"country": "nl",
"browser": "chrome",
}, true) // replace existing contextPass context to one evaluation without changing the instance context:
context := featurevisor.Context{
"userId": "123",
"country": "nl",
}
isEnabled := f.IsEnabled("checkout", context)
variation := f.GetVariation("checkout", context)
variableValue := f.GetVariable("checkout", "bgColor", context)When manually passing context, it will merge with existing context set to the SDK instance before evaluating the specific value.
Check whether a feature is enabled:
featureKey := "checkout"
isEnabled := f.IsEnabled(featureKey)
if isEnabled {
// do something
}Pass additional context to one evaluation:
isEnabled := f.IsEnabled(featureKey, featurevisor.Context{
// ...additional context
})Evaluate a feature's variations:
featureKey := "checkout"
variation := f.GetVariation(featureKey)
if variation != nil && *variation == "treatment" {
// do something for treatment variation
} else {
// handle default/control variation
}Pass additional context to one evaluation:
variation := f.GetVariation(featureKey, featurevisor.Context{
// ...additional context
})Evaluate a feature's variables:
variableKey := "bgColor"
bgColorValue := f.GetVariable("checkout", variableKey)Pass additional context to one evaluation:
bgColorValue := f.GetVariable("checkout", variableKey, featurevisor.Context{
// ...additional context
})Next to generic GetVariable() methods, there are also type specific methods available for convenience:
f.GetVariableBoolean(featureKey, variableKey, context)
f.GetVariableString(featureKey, variableKey, context)
f.GetVariableInteger(featureKey, variableKey, context)
f.GetVariableDouble(featureKey, variableKey, context)
f.GetVariableArray(featureKey, variableKey, context)
f.GetVariableObject(featureKey, variableKey, context)
f.GetVariableJSON(featureKey, variableKey, context)Type specific methods do not coerce values. GetVariableInteger() returns nil for the string "1", and GetVariableBoolean() returns nil for the string "true".
For typed arrays/objects, use Into methods with pointer outputs:
var items []string
_ = f.GetVariableArrayInto(featureKey, variableKey, context, &items)
var cfg MyConfig
_ = f.GetVariableObjectInto(featureKey, variableKey, context, &cfg)context and OverrideOptions are optional and can be passed before the output pointer.
Global variables are independent configuration values. Go uses separate method names because it does not support method overloading:
supportEmail := f.GetGlobalVariableString("supportEmail", context)
settings := f.GetGlobalVariableObject("checkoutSettings", context)
evaluation := f.EvaluateGlobalVariable("supportEmail", context)The complete family includes GetGlobalVariable, the Boolean, string, integer, double, array, object and JSON helpers, GetGlobalVariableKeys, and GetVariableEvaluations. Overrides and conditions are applied for you. See Variables and Conditions for how to define them.
Modules should return the same result for the same input during one evaluation. Pass values that change through context.
To see which overrides changed a value, read VariableOverridePaths from EvaluateGlobalVariable. The singular override fields describe the final match.
Conditions are shared by segments, feature and global variable gates, force entries, traffic rules and variable overrides. Generated v4 datafiles use schema version "4". Arrays imply AND. Groups use and, or and not. NOT negates the AND of its direct children. Empty expressions are false.
Leaves compare context attributes, reference a segment by its literal string key, check a feature's enabled state and optional variation, or compare an evaluated variable. Feature enabled checks default to true. Variable leaves may name a feature owner and use compiled path arrays such as ["items", 0, "enabled"]. Dots in variable keys remain literal. Attribute and variable comparisons use the same operators.
For example, a compiled root gate can combine all four leaf kinds:
{
"conditions": [
{"attribute": "country", "operator": "equals", "value": "nl"},
"eligibleAudience",
{"feature": "checkout", "variation": "treatment"},
{"variable": "policy", "path": ["enabled"], "operator": "equals", "value": true}
]
}Continue using the ordinary getters:
enabled := f.IsEnabled("checkout", featurevisor.Context{"country": "nl"})
policy := f.GetGlobalVariable("policy")Feature evaluation checks sticky state, then force, then its root gate and traffic rules. Force bypasses the gate. Global variables check sticky state and their gate before reading sources or applying overrides. A missed root gate reports conditions_unmet and Evaluation.Conditions. Variation and feature variable evaluations retain their ordinary disabled reasons and fallbacks.
Dependencies share the current evaluation session and a combined depth limit of 256 internal handoffs. Cycles and excessive depth fail the whole evaluation. A later independent call starts a fresh session. Ordinary completed false or null source getter results remain values.
Composed variables read other values through ordinary getters with the effective context, modules and sticky state. The list form creates one fresh outer object with literal property names:
{
"type": "object",
"compose": [
{"key": "enabled", "feature": "checkout"},
{"key": "assignment", "feature": "checkout", "variation": true},
{"key": "title", "feature": "checkout", "variable": "title"},
{"key": "policy", "variable": "policy"}
]
}The string form, "compose": "policy", is an alias of that literal global variable key. It returns the source value directly, including false, zero, empty strings, arrays and null. JSON sources are decoded exactly once, even when the decoded string resembles JSON.
screen := f.GetGlobalVariable("screen", featurevisor.Context{"country": "nl"})
details := f.EvaluateGlobalVariable("screen")Compositions have no defaultValue or useDefaultWhenDisabled. They may have root conditions and disabledValue. An unmet gate skips every source and override, returning the disabled value or null. Whole variable sticky state bypasses both assembly and overrides. An explicit caller default is never forwarded to a source getter.
All sources are read before runtime overrides. Overrides use the ordinary mutation interpreter and provenance fields. The reason is variable_composed, or variable_override_rule when an override matches. Mutation paths copy changed containers without modifying sources, sibling properties or sticky values.
Treat returned objects and arrays as read only. Composition properties and aliases retain ordinary getter ownership. Descendants may be shared. Only the map composition's outer object is guaranteed to be fresh. Source getter failures retain ordinary fallbacks, while dependency cycles and depth failures invalidate the whole composition.
Evaluate all features available in the instance:
allEvaluations := f.GetFeatureEvaluations(featurevisor.Context{}, nil, featurevisor.OverrideOptions{})
fmt.Printf("%+v\n", allEvaluations)
// {
// checkout: {
// enabled: true,
// variation: "control",
// variables: {
// myVariableKey: "myVariableValue",
// },
// },
//
// anotherFeature: {
// enabled: true,
// variation: "treatment",
// }
// }This is handy especially when you want to pass all evaluations from a backend application to the frontend.
Set sticky values to take precedence over feature evaluation against the datafile:
Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides. Child sticky maps replace parent sticky state rather than inheriting it.
Treat supplied sticky maps and their nested values as read only after handoff. Use the instance or child sticky setters to change them. Mutating a supplied map in place is not supported.
import (
"github.com/featurevisor/featurevisor-go/v4"
)
f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
StickyFeatures: &featurevisor.StickyFeatures{
"checkout": {
Enabled: true,
// optional
Variation: func() *featurevisor.VariationValue {
v := featurevisor.VariationValue("treatment")
return &v
}(),
Variables: map[string]interface{}{
"myVariableKey": "myVariableValue",
},
},
"anotherFeatureKey": {
Enabled: false,
},
},
})Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.
Set sticky features after initialization:
f.SetStickyFeatures(featurevisor.StickyFeatures{
"checkout": {
Enabled: true,
Variation: func() *featurevisor.VariationValue {
v := featurevisor.VariationValue("treatment")
return &v
}(),
Variables: map[string]interface{}{
"myVariableKey": "myVariableValue",
},
},
"anotherFeatureKey": {
Enabled: false,
},
}, true) // replace existing sticky features (false by default)
f.SetStickyVariables(featurevisor.StickyVariables{
"supportEmail": "sticky@example.com",
}, true)You may also initialize the SDK without passing datafile, and set it later on:
f.SetDatafile(datafileContent)SetDatafile accepts either parsed featurevisor.DatafileContent or a raw JSON string.
Schema version "2" selectors are normalized eagerly before merging. V4 entities are normalized lazily on first read. Informational schema metadata is retained. Partial updates invalidate only incoming entities, while replacement clears normalized caches. Caller definitions are never changed. Keep supplied datafiles read only after handoff.
It returns true when the datafile is accepted, including unchanged content, and false when the input is rejected or the instance is closed. Rejected input leaves the last accepted datafile in place and reports the existing diagnostic and error event, without a datafile_set event. Calls after closing do nothing. Accepted calls still emit datafile_set, even when nothing changed. The return value reports acceptance, not whether any features changed.
accepted := f.SetDatafile(datafileContent)By default, SetDatafile(datafile) merges the incoming datafile with the SDK instance's existing datafile:
- incoming
Features,Segments, and globalVariablesoverride matching keys - existing
Features,Segments, and globalVariablesthat are missing from the incoming datafile are kept Revision,SchemaVersion, andFeaturevisorVersionare taken from the incoming datafile
Call SetDatafile with different datafiles to merge their features, segments, and global variables.
Pass true as the second argument to replace the stored datafile entirely:
f.SetDatafile(datafileContent, true) // replace existing datafileMerge datafiles for different targets into the same instance. Here, the two datafile values have already been loaded and validated by your application from the same published release:
f.SetDatafile(productsDatafile)
f.SetDatafile(checkoutDatafile)Use immutable release URLs, check HTTP responses and keep shared dependencies consistent. See Loading datafiles on demand for the complete loading and refresh flow.
Each accepted datafile update emits a datafile_set event. Subscribe to react to updates.
The triggers for setting the datafile again can be:
- periodic updates based on an interval (like every 5 minutes), or
- reacting to:
- a specific event in your application (like a user action), or
- an event served via websocket or server-sent events (SSE)
Replace the complete datafile on each refresh so definitions removed upstream are removed from the SDK too. Use merging only when loading partial datafiles intentionally.
import (
"time"
"io"
"net/http"
"github.com/featurevisor/featurevisor-go/v4"
)
func updateDatafile(f *featurevisor.Featurevisor, datafileURL string) {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
resp, err := http.Get(datafileURL)
if err != nil {
continue
}
datafileBytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil || resp.StatusCode != http.StatusOK {
continue
}
var datafileContent featurevisor.DatafileContent
if err := datafileContent.FromJSON(string(datafileBytes)); err != nil {
continue
}
f.SetDatafile(datafileContent, true) // replace
}
}
// Start the update goroutine
go updateDatafile(f, datafileURL)By default, Featurevisor reports diagnostics to the console for info level and above with a [Featurevisor] prefix.
Available diagnostic levels are fatal, error, warn, info, and debug.
Set the level during initialization or update it afterwards:
logLevel := featurevisor.LogLevelDebug
f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
LogLevel: &logLevel,
})
f.SetLogLevel(featurevisor.LogLevelInfo)Use OnDiagnostic to send structured diagnostics to your observability system:
f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
LogLevel: &logLevel,
OnDiagnostic: func(diagnostic featurevisor.FeaturevisorDiagnostic) {
fmt.Println(diagnostic.Level, diagnostic.Code, diagnostic.Message)
},
})Modules can also subscribe to diagnostics or report their own from Setup via the provided module API.
Every diagnostic has Level, Code, Message, and an object-shaped Details map. Optional Module, ModuleName, and OriginalError fields describe provenance. Evaluation metadata belongs in Details.
Malformed stringified selectors report conditions_parse_error at error level once per selector per incoming entity load. Details contain exactly conditions with the original string, entityType and entityKey. Schema 2 diagnostics run after accepted datafile storage. V4 diagnostics run after caching the first entity read. Handlers may safely read the accepted revision again. A malformed selector fails locally, even through NOT or segment references, without rejecting the datafile or disabling unrelated entities.
Diagnostic handlers are isolated from SDK behaviour. A panic in a handler does not stop other handlers or evaluations.
Subscribe to SDK events to respond to context, sticky state and datafile changes.
Subscribe to instance lifecycle events:
unsubscribe := f.On(featurevisor.EventNameDatafileSet, func(details featurevisor.EventDetails) {
revision := details["revision"] // new revision
previousRevision := details["previousRevision"]
revisionChanged := details["revisionChanged"] // true if revision has changed
// list of feature keys that have new updates,
// and you should re-evaluate them
features := details["features"]
variables := details["variables"]
// handle here
})
// stop listening to the event
unsubscribe()The features and variables arrays include direct changes and transitive dependencies through conditions, segments and composition sources, including additions and removals.
- added, or
- updated, or
- removed
compared to the previous datafile content that existed in the SDK instance.
unsubscribe := f.On(featurevisor.EventNameContextSet, func(details featurevisor.EventDetails) {
replaced := details["replaced"] // true if context was replaced
context := details["context"] // the new context
fmt.Println("Context set")
})unsubscribe := f.On(featurevisor.EventNameStickyFeaturesSet, func(details featurevisor.EventDetails) {
replaced := details["replaced"] // true if sticky features got replaced
features := details["features"] // list of all affected feature keys
fmt.Println("Sticky features set")
})unsubscribe := f.On(featurevisor.EventNameStickyVariablesSet, func(details featurevisor.EventDetails) {
variables := details["variables"]
replaced := details["replaced"]
fmt.Println(variables, replaced)
})unsubscribe := f.On(featurevisor.EventNameError, func(details featurevisor.EventDetails) {
diagnostic := details["diagnostic"]
fmt.Println(diagnostic)
})The error event is emitted for diagnostics whose level is error.
Inspect how variations and variables are evaluated against a context:
// flag
evaluation := f.EvaluateFlag(featureKey, context)
// variation
evaluation := f.EvaluateVariation(featureKey, context)
// variable
evaluation := f.EvaluateVariable(featureKey, variableKey, context)The returned object will always contain the following properties:
Type: flag, variation or variableFeatureKey: the feature key when the evaluation has a feature ownerReason: the reason how the value was evaluated
And optionally these properties depending on whether you are evaluating a feature variation or a variable:
BucketValue: the bucket value between 0 and 100,000RuleKey: the rule keyError: the error objectEnabled: if feature itself is enabled or notVariation: the variation objectVariationValue: the variation valueVariableKey: the variable keyVariableValue: the variable valueVariableSchema: the variable schemaVariableOverrideIndex: index of matched variable override when applicableConditions: normalized root conditions when the gate is unmetGlobalVariable: the global variable definition when applicableStickyFeature: the sticky feature result when applicableVariableOverrideKey: key of the final matched variable overrideVariableOverridePath: path of the final matched variable overrideVariableOverridePaths: all matched accumulated override paths in execution order
The before evaluation and after evaluation phases each use the module membership at the start of that phase. Where an evaluation uses bucketing, the bucket key and bucket value phases each use their own starting membership. Adding or removing a module during a phase does not change the iteration already in progress. The next phase uses the membership then installed. A module added before the after phase can receive after callbacks for evaluations already in progress, even if it did not receive their before callbacks. A module removed before the after phase receives no after callback.
Modules allow you to intercept the evaluation process and customize it further as per your needs.
For every feature or global variable evaluation, all BeforeEvaluation callbacks run in registration order before evaluation, followed by all AfterEvaluation callbacks in registration order. Condition and composition dependency getters use the same pipeline. An accumulated override program is one evaluation, so callbacks wrap the complete program rather than individual operations.
EvaluateOptions.DependencyOf and DependencyPurpose are absent for an application call. Internal reads identify the immediate requester through Type, optional FeatureKey and optional VariableKey. Purpose is condition for gating reads and composition for consumed values. Tracking modules can skip condition reads while retaining composition exposures. Conditions preserve their written order and short circuit, so put cheap comparisons first when appropriate.
Equivalent dependency reads are memoized within one public call, separating purpose, requester, effective context and current SDK state. Condition snapshots are owned. Composition values keep ordinary getter ownership. Callback options do not retain private sessions. Independent public calls from callbacks start fresh sessions.
A module is a simple struct with a recommended unique Name and optional functions:
If Setup panics, the module is not registered. Featurevisor removes subscriptions created during setup, reports module_setup_error, and calls Close when present.
import (
"github.com/featurevisor/featurevisor-go/v4"
)
myCustomModule := &featurevisor.FeaturevisorModule{
// recommended for diagnostics and removal
Name: "my-custom-module",
// rest of the properties below are all optional per module
Setup: func(api featurevisor.FeaturevisorModuleApi) {
api.ReportDiagnostic(featurevisor.FeaturevisorModuleReportedDiagnostic{
Level: featurevisor.LogLevelInfo,
Code: "module_ready",
Message: "Module is ready",
})
},
// before a feature or global variable evaluation
BeforeEvaluation: func(options featurevisor.EvaluateOptions) featurevisor.EvaluateOptions {
// update context before evaluation
if options.Context == nil {
options.Context = featurevisor.Context{}
}
options.Context["someAdditionalAttribute"] = "value"
return options
},
// after a feature or global variable evaluation
AfterEvaluation: func(evaluation featurevisor.Evaluation, options featurevisor.EvaluateOptions) featurevisor.Evaluation {
if evaluation.Reason == "error" {
// log error
return evaluation
}
return evaluation
},
// configure bucket key
BucketKey: func(options featurevisor.ConfigureBucketKeyOptions) featurevisor.BucketKey {
// return custom bucket key
return options.BucketKey
},
// configure bucket value (between 0 and 100,000)
BucketValue: func(options featurevisor.ConfigureBucketValueOptions) featurevisor.BucketValue {
// return custom bucket value
return options.BucketValue
},
Close: func() {
// clean up module resources
},
}Register modules when you create the instance:
import (
"github.com/featurevisor/featurevisor-go/v4"
)
f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Modules: []*featurevisor.FeaturevisorModule{
myCustomModule,
},
})Or after initialization:
removeModule := f.AddModule(myCustomModule)
removeModule()A child snapshots the parent keys that exist when it is spawned. Child values win for those keys. Parent keys introduced later are still inherited. Calling Close() removes both child-owned listeners and subscriptions delegated to the parent.
When dealing with purely client-side applications, it is understandable that there is only one user involved, like in browser or mobile applications.
But when using Featurevisor SDK in server-side applications, where a single server instance can handle multiple user requests simultaneously, it is important to isolate the context for each request.
That's where child instances come in handy:
childF := f.Spawn(featurevisor.Context{
// user or request specific context
"userId": "123",
})Pass the child instance to the request handler to evaluate features for that user:
isEnabled := childF.IsEnabled("checkout")
variation := childF.GetVariation("checkout")
variableValue := childF.GetVariable("checkout", "bgColor")Similar to parent SDK, child instances also support several additional methods:
SetContextSetStickyFeaturesEvaluateFlagIsEnabledEvaluateVariationGetVariationEvaluateVariableGetVariableGetVariableBooleanGetVariableStringGetVariableIntegerGetVariableDoubleGetVariableArrayGetVariableArrayIntoGetVariableObjectGetVariableObjectIntoGetVariableJSONOnClose
Both primary and child instances support a .Close() method, that removes forgotten event listeners (via On method) and cleans up any potential memory leaks.
f.Close()The OpenFeature provider is a separate Go module, so applications that do not use OpenFeature do not receive its dependencies. The provider requires Go 1.26 or newer and uses OpenFeature Go SDK 1.19.0. The core Featurevisor SDK still supports Go 1.21:
go get github.com/featurevisor/featurevisor-go/openfeature/v4import (
"context"
featurevisor "github.com/featurevisor/featurevisor-go/v4"
featurevisorof "github.com/featurevisor/featurevisor-go/openfeature/v4"
of "github.com/open-feature/go-sdk/openfeature"
)
provider := featurevisorof.NewProvider(featurevisorof.Options{
FeaturevisorOptions: featurevisor.FeaturevisorOptions{
Datafile: datafileContent,
},
})
if err := of.SetProviderAndWait(provider); err != nil {
panic(err)
}
client := of.NewClient("")
enabled, err := client.BooleanValue(
context.Background(),
"checkout",
false,
of.NewEvaluationContext("user-123", map[string]any{"country": "nl"}),
)Use checkout for a flag, checkout:variation for its variation, checkout:title for a feature variable, and variable:supportEmail for a global variable. Boolean variables use the boolean resolver. Arrays, objects, and JSON variables use the object resolver.
OpenFeature's targeting key maps to userId by default. TargetingKeyField, KeySeparator, VariationKey, and GlobalVariablePrefix can customize the mapping. The global prefix defaults to variable and cannot contain the separator. The provider's separate module follows the Go version requirement of the official OpenFeature Go SDK. A provider call receiving a FlattenedContext with an explicitly empty targetingKey overrides the configured field. The upstream Go client omits empty targeting keys when flattening EvaluationContext, so NewEvaluationContext("", attributes) cannot express the difference between absent and explicitly empty. To override an existing field with an empty key through the client, include "targetingKey": "" in its attributes. If a nonempty targeting key is inherited into the final context, clear it at its source first. A narrower empty key may not remove it.
Root conditions_unmet outcomes map to OpenFeature DISABLED. Composed values and matched overrides map to TARGETING_MATCH. The provider does not decode composed JSON values again.
Reuse an existing Featurevisor instance:
f := featurevisor.CreateFeaturevisor(featurevisor.FeaturevisorOptions{
Datafile: datafileContent,
})
provider := featurevisorof.NewProvider(featurevisorof.Options{
Featurevisor: f,
})The caller owns an instance passed this way. Provider shutdown does not close it. Call fv.Close() when every consumer is finished with it. When the provider creates the instance from FeaturevisorOptions, the provider owns and closes it. If both fields are supplied, Featurevisor takes precedence over FeaturevisorOptions.
An invalid initial datafile in an owned instance makes SetProviderAndWait return a parse error, independently of the diagnostic log level. Valid updates clear the error and emit readiness. Shutdown removes provider subscriptions and closes only an owned instance.
See the OpenFeature provider guide for resolution reasons, errors, metadata, tracking, lifecycle, and providers for other languages.
Build the executable from this SDK repository:
go build -o featurevisor ./cmdThe native test runner requires @featurevisor/cli 4.0.0 or later and Node.js 24 or later. Install the Node.js CLI in your Featurevisor project:
npm install --save-dev @featurevisor/cli@^4.0.0The native CLI supports test, benchmark and assess-distribution. Node.js remains responsible for parsing definitions, listing tests through the execution protocol, expanding matrices and generating datafiles through npx featurevisor. Evaluations run through this Go SDK. Use the Node.js CLI directly for authoring commands such as init, lint and build. The SDK runtime has no Node.js dependency.
Options use kebab case, such as --project-directory-path. The camelCase forms from earlier releases still work, and --root-directory-path is an alias for --project-directory-path. Pass values as --name=value or --name value. Repeat --target to select several targets in order. Run <command> --help to list every option.
Quote JSON and paths containing spaces as a single shell argument. Duplicate targets are evaluated once, at their first position. Extra positional arguments and unknown options are rejected.
./featurevisor test \
--project-directory-path=/path/to/featurevisor-project \
--only-failuresSee Testing for authoring specs and Checks for check syntax and operators.
Segment owners and checks use ordinary SDK gates, including feature and variable dependencies, fixed at, context and sticky state. Selected Target segment specializations remain authoritative. Compiled execution definitions restore missing segments. Attribute only segment chains can run without an environment. Dependent segments need their selected datafile. Every transitive check reference is preflighted before getters run. Synthetic segment gates are excluded from coverage. Testing does not automatically run lint.
Useful test options:
--key-pattern=<pattern>and--assertion-pattern=<pattern>filter tests.--entity-type=scenarioselects scenarios.--set=<set>selects one project set. Omitting--setruns all sets.--case=<id>reruns one expanded case. Copy the opaque ID or rerun command from a failure report.--json --prettyproduces an indented JSON report.--coveragereports observed rule and override selections.--uncoveredlists features and global variables without associated specs.--only-failures,--quiet,--verboseand--show-datafilecontrol diagnostic output.
The examples below use a project without environments. Add --environment=<name> if your project configures environments. In a sets project, omit --set to run every set or pass --set=<name>. Repeat --target=<key> to evaluate specific target datafiles. The Node.js CLI validates these options and builds the datafiles. Go evaluates them. Replace checkout with a feature defined in your project.
./featurevisor benchmark \
--project-directory-path=/path/to/featurevisor-project \
--feature=checkout \
--context='{"country":"nl"}' \
--n=1000Use --variation for a variation or --variable=<key> for a feature variable. Omit --feature to benchmark a global variable. Do not combine --variation with --variable. See Benchmarking.
./featurevisor assess-distribution \
--project-directory-path=/path/to/featurevisor-project \
--feature=checkout \
--populate-uuid=userId \
--n=1000Repeat --populate-uuid=<key> or --populate-uuid <key> to generate UUID values for multiple context attributes. --n and -n both accept a positive integer and default to 1000 evaluations in this native CLI. See Assess distribution.
- SDK: featurevisor/featurevisor-go
- Example application: featurevisor/featurevisor-example-go
The aggregate evaluation reference has moved to Getting evaluations.
The sticky state reference has moved to Sticky.
Clone the repository, and install the dependencies using Go modules:
go mod downloadmake check
make test-example-1The native tests cover runtime conformance and project integration.
Run go test -run TestReadmeInitialization . to compile and execute the actual initialization snippet against a local HTTP fixture. This check also runs in the ordinary SDK suite and CI.
- Tag the core SDK as
v4.x.y. - Tag the provider module separately as
openfeature/v4.x.y. - Release the core SDK before the provider that depends on it.
- Run
make verify-packagesbefore creating either release. - Create the matching releases on GitHub.
MIT © Fahad Heylaal