Skip to main content

Go SDK Troubleshooting

Start by separating client construction, background definition refresh, and local evaluation. A successful NewClient call does not mean definitions have arrived. An evaluation returning false, nil can mean a disabled flag, an unknown key, or definitions that have not loaded yet.

Installation and imports​

Run these commands inside the directory containing your application's go.mod:

go get github.com/ops-ai/Toggly.FeatureManagement/toggly-go@latest
go list -m github.com/ops-ai/Toggly.FeatureManagement/toggly-go

Import github.com/ops-ai/Toggly.FeatureManagement/toggly-go/toggly in Go code. The module path and package path differ. If resolution fails, check the module path, your Go toolchain, and your GOPROXY configuration. See the Go SDK overview for requirements.

Construction, loading, and missing keys​

This complete program checks configuration before constructing the client and uses a disabled fallback. Set TOGGLY_APP_KEY in your application's environment; an absent key is a setup problem, not a reason to create a placeholder client.

package main

import (
"context"
"errors"
"log"
"os"
"strings"
"time"

"github.com/ops-ai/Toggly.FeatureManagement/toggly-go/toggly"
)

func run() error {
appKey := strings.TrimSpace(os.Getenv("TOGGLY_APP_KEY"))
if appKey == "" {
return errors.New("set TOGGLY_APP_KEY before starting the application")
}

client, err := toggly.NewClient(toggly.Config{
AppKey: appKey,
Environment: "Production", // Match the environment name in Toggly.
RefreshInterval: 2 * time.Minute,
HTTPTimeout: 5 * time.Second,
EnableUndefinedOnDevelopment: false,
})
if err != nil {
return errors.New("could not create the Toggly client; check configuration")
}
// This function owns the client. Close it exactly once when work finishes.
defer func() { _ = client.Close() }()

enabled, err := client.IsEnabled(context.Background(), "new-dashboard", toggly.Context{
Identity: "user-123",
Groups: []string{"beta"},
Claims: map[string]string{"plan": "pro"},
})
if err != nil {
log.Print("feature evaluation failed; using the disabled fallback")
enabled = false
}
// A first check can be false while the asynchronous refresh is still running.
log.Printf("new-dashboard enabled: %t", enabled)
return nil
}

func main() {
if err := run(); err != nil {
log.Print(err) // run returns only the fixed, configuration-safe messages above.
os.Exit(1)
}
}

A short-lived program can finish before its initial fetch. In a server, construct one client during application startup and keep it alive across requests. Register its cleanup with the application owner, after request work has stopped.

The SDK starts its initial refresh asynchronously, then refreshes periodically. There is no public Init, InitWithContext, WaitForSync, Refresh, or RefreshWithContext method. Do not add a second refresh goroutine. DisableBackgroundRefresh: true also prevents the initial fetch and snapshot load; it is not a manual-refresh mode.

Unknown keys return false, nil by default. An empty feature key returns an error. Keep EnableUndefinedOnDevelopment false in production: setting it true enables unknown flags, including flags absent during loading. Your application must decide whether to serve its disabled fallback or hold traffic while awaiting definitions. A flag's boolean result alone cannot make that readiness decision.

Inspect refresh status safely​

Add this function to an application that imports log and toggly:

func reportRefreshState(client *toggly.Client) {
state := client.ProviderDebugInfo()
log.Printf("definitions=%d successful_refresh=%t refresh_error_recorded=%t",
state.DefinitionsCount, state.LastRefresh != nil, state.LastError != "")
}

Interpret the fields together:

  • LastRefresh == nil: no successful network refresh has been recorded. A snapshot may already supply definitions; this is not proof that evaluation is empty.
  • DefinitionsCount == 0: no definitions are currently loaded. This can also be a successful response for an environment with no flags.
  • LastRefresh: time of the latest successful refresh, including a cache-validating response. Use its age for your application's freshness policy.
  • LastError and LastErrorTime: the most recent recorded refresh failure. They are retained after a later success; compare timestamps rather than treating any non-empty error string as a current outage.

The status object includes AppKey, and raw errors can include request URLs. Log selected fields as above instead of printing the whole object or raw error. Keep detailed diagnostics restricted and redact keys, URLs, identities, and claims before sharing them with support. The config has no Debug or Logger option.

For connectivity failures, check the configured environment and key, DNS, TLS, proxy/firewall access to the definitions host, and DefinitionsURL. Use HTTPTimeout to bound definition HTTP requests. NewClient does not return the asynchronous fetch error, and a local IsEnabled check does not retry that fetch. Existing definitions are retained if a later network refresh fails.

Context and cancellation​

context.Context carries cancellation and ambient context. toggly.Context carries targeting data. Pass both to IsEnabled; the targeting value is not a pointer. The SDK evaluates loaded definitions locally and does not universally return context.Canceled for a canceled caller. Context-aware session stores and authorization hooks receive the caller context, but local evaluation is not a network request and cancellation cannot interrupt every filter.

This application-owned helper checks cancellation before calling the SDK and again before returning a result. Add it alongside imports for context and toggly. It cannot forcibly stop an SDK call or a custom hook that ignores context.

type FeatureManager interface {
IsEnabled(context.Context, string, toggly.Context) (bool, error)
}

var _ FeatureManager = (*toggly.Client)(nil)

func enabledForRequest(ctx context.Context, flags FeatureManager, key string, user toggly.Context) (bool, error) {
if err := ctx.Err(); err != nil {
return false, err
}
enabled, err := flags.IsEnabled(ctx, key, user)
if canceled := ctx.Err(); canceled != nil {
return false, canceled
}
if err != nil {
return false, err
}
return enabled, nil
}

In an HTTP handler, pass r.Context() to this helper. If the request is canceled, stop its work; for other evaluation errors, select your application's safe fallback and log a fixed message. Avoid context.Background() for request-bound checks. A request deadline affects this request's work, not the client's background refresh.

Targeting and concurrent requests​

Ordinary boolean evaluation supports a shared client with a separate context for each call. This function uses the helper above; add the time import:

func premiumEnabled(ctx context.Context, flags FeatureManager, identity string) (bool, error) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()

return enabledForRequest(ctx, flags, "premium-feature", toggly.Context{
Identity: identity,
Groups: []string{"premium"},
Claims: map[string]string{"plan": "premium"},
Traits: map[string]any{"country": "US"},
})
}

Build groups and claims from trusted application data. Do not mutate or reuse request context maps while evaluations are running. Register custom filters during startup, and ensure your custom filters, session store, and authorization service are safe for concurrent calls. For domain objects such as an order, use the entity context API.

Remote variants use a different, client-scoped context. Configure all known values before construction, as in this helper:

func newVariantsClient(appKey, identity string, groups []string, claims map[string]string) (*toggly.Client, error) {
return toggly.NewClient(toggly.Config{
AppKey: appKey,
Environment: "Production",
EnableVariants: true,
VariantIdentity: identity,
VariantGroups: groups,
VariantClaims: claims,
})
}

The caller checks the returned error, owns the client, and closes it exactly once. The SDK copies groups and claims before refreshing. Use a variants client only for its fixed context; never call SetVariantIdentity per HTTP request on a shared client. Passing a different toggly.Context to IsEnabled does not retarget the server-evaluated variant. GetVariant returns nil if no named variant is loaded, so choose an application fallback. See Configuration.

Caching, telemetry, and shutdown​

Evaluations already use definitions held in memory; there is no EnableCache, CacheTTL, or HTTPClient field in toggly.Config. Tune RefreshInterval and HTTPTimeout, and use a SnapshotProvider for persistence. Snapshot loading happens during asynchronous refresh, so configuring a provider alone is not a synchronous readiness guarantee. Use separate snapshot storage for different applications and environments.

Use SessionStore and SessionTTL for supported sticky rollouts; these do not control how fresh the definitions are. For multiple flags, loop over IsEnabled and handle each error, or use EvaluateGate for an Any/All gate. Neither approach forces a network fetch or promises one atomic revision across calls.

Set EnableUsage or EnableMetrics when constructing the client to enable the optional gRPC pipelines. Metric emission checks for the optional client. Pass the variant name when applicable, or an empty string for a boolean feature:

func recordCheckoutLatency(client *toggly.Client, elapsed time.Duration, variant string) {
if metrics := client.MetricsClient(); metrics != nil {
featureKey := "ExpressCheckout"
metrics.Measure("Checkout.LatencyMs", float64(elapsed.Microseconds())/1000, &featureKey, variant)
}
}

See Usage statistics & metrics for configuration and flushing. Close stops the refresh loop and closes optional telemetry clients. Call it once from the owning component, after in-flight application work finishes. It waits for the refresh goroutine, so shutdown can wait on an active fetch; it does not accept a cancellation context. Do not defer it in every request handler or close it again from a second shutdown path.

Test the application boundary​

The interface above matches the real client. Put this test in the same package as enabledForRequest, with imports for context, testing, and toggly:

type fakeFlags struct {
calls int
}

func (f *fakeFlags) IsEnabled(_ context.Context, _ string, _ toggly.Context) (bool, error) {
f.calls++
return true, nil
}

func TestCanceledRequestDoesNotEvaluate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
flags := &fakeFlags{}

enabled, err := enabledForRequest(ctx, flags, "new-dashboard", toggly.Context{})
if err != context.Canceled || enabled || flags.calls != 0 {
t.Fatalf("expected cancellation without evaluation; enabled=%t calls=%d", enabled, flags.calls)
}
}

Also test denied and missing flags, evaluation errors, requests with different identities, loading before the first response, snapshot startup, refresh failure and recovery, and shutdown. Use a local HTTP fixture with the real SDK for refresh tests; the fake above tests only your application's cancellation boundary.

Getting more help​