OpenFeature Go Provider
Introduction
The OpenFeature Go Provider is intended to be used in combination with the OpenFeature Go SDK. The provider wraps the ConfigDirector Go SDK.
The minimum Go version supported is 1.26.
The provider is safe to share between goroutines. Register one instance per process when your application starts, and shut OpenFeature down when it stops. Evaluations read config state the provider already holds in memory, so they make no network calls on the request path.
Installation
The provider can be installed with go get. Its package reference is on pkg.go.dev: https://pkg.go.dev/github.com/ConfigDirector/go-sdks/configdirector-openfeature
The OpenFeature Go SDK (github.com/open-feature/go-sdk) and the ConfigDirector Go SDK are included as dependencies.
go get github.com/ConfigDirector/go-sdks/configdirector-openfeature@latest
Configure and initialize the client
- Create an instance of the provider using your server SDK key. You can retrieve a server SDK key for each environment under
SDK Keysin the dashboard's navigation panel. The provider starts connecting as soon as it is created. - Set the OpenFeature provider.
- Get a client instance from OpenFeature.
import (
"log"
"github.com/ConfigDirector/go-sdks/configdirector"
configdirectoropenfeature "github.com/ConfigDirector/go-sdks/configdirector-openfeature"
"github.com/open-feature/go-sdk/openfeature"
)
// IMPORTANT: Do not commit the server SDK key to your source code, it is a secret value.
provider, err := configdirectoropenfeature.NewProvider(configdirector.Options{SDKKey: "YOUR-SERVER-SDK-KEY"})
if err != nil {
log.Fatal(err)
}
// Blocks until the provider is initialized or it times out.
// If initialization times out, the provider keeps connecting in the background.
if err := openfeature.SetProviderAndWait(provider); err != nil {
log.Fatal(err)
}
defer openfeature.Shutdown()
client := openfeature.NewClient("my-app")
SetProviderAndWait blocks until the initial config state arrives or the configured timeout elapses, and it does not return an error on a connection failure. Until config state arrives, evaluations return the in-code default value with the PROVIDER_NOT_READY error code, and the provider continues to connect in the background. SetProviderWithContextAndWait takes a context that abandons the wait when it ends.
Additional configuration options
The provider takes the same configdirector.Options as the Go SDK client, so every option of the SDK is available on the provider.
For example, the Metadata can be provided like this:
provider, err := configdirectoropenfeature.NewProvider(configdirector.Options{
SDKKey: "YOUR-SERVER-SDK-KEY",
Metadata: configdirector.Metadata{AppName: "YOUR-APP-NAME", AppVersion: "1.0.2"},
})
The provider accepts the same Metadata, Logger, Connection, Telemetry and Hooks options as the Go SDK client, refer to the additional configuration options section of the Go SDK for a full list. Handlers in Hooks run after the provider's own.
Shut down
Shutting OpenFeature down closes the provider, which closes its connections and reports any pending telemetry:
defer openfeature.Shutdown()
Retrieve config values
To retrieve config values, use the OpenFeature client:
Every getter takes a context.Context first, such as the request's context in an HTTP handler, and the evaluation context last:
booleanValue := client.Boolean(ctx, "my-config-key", false, openfeature.EvaluationContext{})
stringValue := client.String(ctx, "my-string-config-key", "Default", openfeature.EvaluationContext{})
Each OpenFeature getter maps to a ConfigDirector config type:
| OpenFeature getter | ConfigDirector config value |
|---|---|
Boolean | Boolean |
String | String or enum |
Int, Float | Number |
Object | JSON object or JSON array |
Object returns the JSON document decoded the way encoding/json decodes into an any: a JSON object is a map[string]any, a JSON array a []any, and a number a float64:
settings := client.Object(ctx, "my-json-config-key", map[string]any{}, openfeature.EvaluationContext{})
theme := settings.(map[string]any)["theme"]
For additional information regarding the OpenFeature client refer to the OpenFeature Go SDK documentation.
Evaluation details
The detailed getters of the OpenFeature client, such as BooleanValueDetails, report why an evaluation produced the value that it did:
| Outcome | Reason | Error code |
|---|---|---|
| A value was found | TARGETING_MATCH | |
| The config carries no value | DEFAULT | |
| The config key is unknown | ERROR | FLAG_NOT_FOUND |
| No config state has arrived yet | ERROR | PROVIDER_NOT_READY |
| The value does not match the requested type | ERROR | TYPE_MISMATCH |
When a value was found, the Variant is ConfigDirector's identifier for that value. In every other case the in-code default value is returned.
details, err := client.BooleanValueDetails(ctx, "new-checkout", false, user)
if err != nil {
log.Printf("new-checkout fell back to its in-code default value: %s", details.ErrorCode)
}
User context
The user context is the last argument of every getter of the OpenFeature client. The OpenFeature Go provider evaluates targeting rules locally without additional network calls for different contexts.
import "github.com/open-feature/go-sdk/openfeature"
user := openfeature.NewEvaluationContext(
"12345", // In OpenFeature, the targeting key represents the context's user ID
map[string]any{
"name": "Example User",
// Any arbitrary traits which can be referenced in targeting rules
"traits": map[string]any{"region": "North America"},
},
)
booleanValue := client.Boolean(ctx, "my-config-key", false, user)
The evaluation context maps onto the ConfigDirector user context as follows:
| OpenFeature evaluation context | ConfigDirector user context |
|---|---|
The targeting key, or otherwise an id attribute | ID |
The name attribute | Name |
The traits attribute, a map[string]any | Traits |
The anonymous attribute, a bool | Anonymous |
For additional information regarding the OpenFeature client refer to the OpenFeature Go SDK documentation.
Events
The provider emits PROVIDER_CONFIGURATION_CHANGED whenever configs are updated on the dashboard or via the admin API, carrying the keys of the configs in the update followed by the keys of the configs it removed:
logChangedFlags := func(details openfeature.EventDetails) {
log.Printf("Configs updated: %v", details.FlagChanges)
}
client.AddHandler(openfeature.ProviderConfigChange, &logChangedFlags)
It emits PROVIDER_READY when the initial config state arrives after SetProviderAndWait has already returned.
Test your code
Code that reads flags through OpenFeature is tested by swapping the provider, so it never has to change for a test and nothing from ConfigDirector is involved. The OpenFeature Go SDK ships an in-memory provider for that, memprovider, which serves the values your test sets.
Each flag is an InMemoryFlag with its named variants and the name of the variant it resolves to:
import (
"testing"
"github.com/open-feature/go-sdk/openfeature"
"github.com/open-feature/go-sdk/openfeature/memprovider"
"github.com/stretchr/testify/require"
)
func TestShowsTheNewCheckout(t *testing.T) {
provider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{
"new-checkout": {Key: "new-checkout", State: memprovider.Enabled, DefaultVariant: "on", Variants: map[string]any{"on": true, "off": false}},
"max-items": {Key: "max-items", State: memprovider.Enabled, DefaultVariant: "default", Variants: map[string]any{"default": int64(20)}},
})
require.NoError(t, openfeature.SetProviderAndWait(provider))
t.Cleanup(openfeature.Shutdown)
client := openfeature.NewClient("checkout")
require.True(t, client.Boolean(t.Context(), "new-checkout", false, openfeature.EvaluationContext{}))
require.Equal(t, int64(20), client.Int(t.Context(), "max-items", 0, openfeature.EvaluationContext{}))
}
The in-memory provider has no method to change a flag, so a test that changes a value mid-way sets a new provider with SetProviderAndWait. A flag can also carry a ContextEvaluator, a pointer to a function from the flag and the evaluation context to the value and its resolution details, for a test of code that targets specific users. The OpenFeature API is one global, so shut it down after each test with openfeature.Shutdown, as the cleanup above does, which lets the next test register its own provider. The examples use require from the testify module; the standard testing package works just as well.