Skip to content

Go SDK

Installation

bash
go get github.com/phaseflag/go-sdk

Requires Go 1.21+


Initialization

go
package main

import (
    "log"
    "time"

    phaseflag "github.com/phaseflag/go-sdk"
)

func main() {
    client := phaseflag.NewClient(phaseflag.Config{
        BaseURL:         "https://api.phaseflag.com/api/v1", // including /api/v1
        APIKey:          "sdk-dev-xxxxxxxxxxxx",
        PollingInterval: 30 * time.Second,                  // default: 30s
    })

    // Fetch the initial ruleset and start background goroutines
    if err := client.Start(); err != nil {
        log.Fatalf("failed to start Phase Flag client: %v", err)
    }
    defer client.Stop()

    // Block until the first ruleset fetch completes (or timeout)
    if !client.WaitUntilReady(5 * time.Second) {
        log.Println("warning: Phase Flag client did not become ready in time")
    }
}

All Get* methods are safe to call from multiple goroutines without additional synchronization.


Evaluating Flags

Boolean flag

go
ctx := &phaseflag.EvaluationContext{
    UserID: "user-123",
}

// Returns false if flag is off, not found, or not a bool
enabled := client.GetBooleanValue("new-checkout-flow", false, ctx)

if enabled {
    renderNewCheckout(w, r)
} else {
    renderLegacyCheckout(w, r)
}

String flag

go
theme := client.GetStringValue("ui-theme", "light", ctx)
// Returns "dark", "light", or "system"
applyTheme(theme)

JSON flag

go
config := client.GetJsonValue("checkout-config", map[string]interface{}{
    "provider": "stripe",
}, ctx)

// Type-assert as needed
if configMap, ok := config.(map[string]interface{}); ok {
    fmt.Println(configMap["provider"]) // "stripe"
}

Full evaluation result

go
result := client.GetVariation("new-checkout-flow", ctx)
if result != nil {
    fmt.Printf("Value:        %v\n", result.Value)
    fmt.Printf("Reason:       %s\n", result.Reason)
    fmt.Printf("VariationKey: %s\n", result.VariationKey)
}

EvaluationContext

go
ctx := &phaseflag.EvaluationContext{
    UserID:    "user-123",   // used for deterministic percentage bucketing
    SessionID: "sess-abc",   // fallback when UserID is empty
    Attributes: map[string]interface{}{
        "email":       "alice@example.com",
        "plan":        "pro",
        "country":     "US",
        "app_version": "2.4.1",
        "account_age": 365,
    },
}

Attribute values can be string, int, float64, or bool.


Concurrent-Safe Usage

go
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func(userID string) {
        defer wg.Done()
        evalCtx := &phaseflag.EvaluationContext{UserID: userID}
        enabled := client.GetBooleanValue("feature-x", false, evalCtx)
        _ = enabled
    }(fmt.Sprintf("user-%d", i))
}

wg.Wait()

HTTP Middleware Integration

go
func FeatureFlagMiddleware(client *phaseflag.Client) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            userID := r.Header.Get("X-User-ID")

            evalCtx := &phaseflag.EvaluationContext{
                UserID: userID,
                Attributes: map[string]interface{}{
                    "country": r.Header.Get("CF-IPCountry"),
                    "plan":    r.Header.Get("X-User-Plan"),
                },
            }

            enabled := client.GetBooleanValue("new-api-handler", false, evalCtx)

            ctx := context.WithValue(r.Context(), "newAPIEnabled", enabled)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Listen for Flag Changes

Register a callback that fires whenever the ruleset is updated after a poll:

go
unsubscribe := client.OnFlagsChanged(func(flags []phaseflag.FlagDefinition) {
    fmt.Printf("Ruleset updated: %d flags loaded\n", len(flags))
    // Re-evaluate and update application state
})

// Remove the listener when no longer needed
unsubscribe()

Remote Evaluation

Evaluate a flag server-side (sends the context to the API):

go
result, err := client.Evaluate("new-checkout-flow", &phaseflag.EvaluationContext{
    UserID: "user-123",
})
if err != nil {
    log.Printf("remote evaluation error: %v", err)
}

fmt.Printf("Value:  %v\n", result.Value)
fmt.Printf("Reason: %s\n", result.Reason)

Flag Mocking (Testing)

Override flag values in tests without connecting to the API:

go
// Empty BaseURL disables HTTP — safe for unit tests
client := phaseflag.NewClient(phaseflag.Config{})

client.SetOverride("new-checkout-flow", true)

enabled := client.GetBooleanValue("new-checkout-flow", false, nil)
// enabled == true

client.ClearOverride("new-checkout-flow")
client.ClearAllOverrides()

Bootstrap Loading

Load flags from a file or URL for cold-start resilience:

go
// From a local file
client := phaseflag.NewClient(phaseflag.Config{
    BaseURL:       "https://api.phaseflag.com/api/v1",
    APIKey:        "sdk-dev-xxxxxxxxxxxx",
    BootstrapFile: "/etc/phaseflag/bootstrap.json",
})

// From a URL
client := phaseflag.NewClient(phaseflag.Config{
    BaseURL:      "https://api.phaseflag.com/api/v1",
    APIKey:       "sdk-dev-xxxxxxxxxxxx",
    BootstrapURL: "https://cdn.example.com/flags/bootstrap.json",
})

Offline Mode

go
client := phaseflag.NewClient(phaseflag.Config{
    BaseURL:       "https://api.phaseflag.com/api/v1",
    APIKey:        "sdk-dev-xxxxxxxxxxxx",
    OfflineMode:   true,
    BootstrapFile: "/etc/phaseflag/bootstrap.json",
})
client.Start()
// If the API is unreachable, the client uses bootstrap data and becomes
// ready immediately.

Event Tracking

go
client.TrackEvent(phaseflag.EvaluationEvent{
    FlagKey:      "new-checkout-flow",
    VariationKey: "enabled",
    UserID:       "user-123",
})

// Flush all queued events immediately
if err := client.FlushEvents(); err != nil {
    log.Printf("flush error: %v", err)
}

Configuration Reference

go
phaseflag.Config{
    BaseURL:            "https://api.phaseflag.com/api/v1", // required (including /api/v1)
    APIKey:             "sdk-dev-xxxxxxxxxxxx",            // required
    PollingInterval:    30 * time.Second,                  // optional, default: 30s
    EventFlushInterval: 30 * time.Second,                  // optional, default: 30s
    EventBatchSize:     100,                               // optional, default: 100
    BootstrapFile:      "/path/to/bootstrap.json",         // optional
    BootstrapURL:       "https://cdn.example.com/b.json",  // optional
    OfflineMode:        false,                             // optional
}

Graceful Shutdown

go
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
<-sigCh

// Stop blocks until background goroutines exit and events are flushed
client.Stop()

Released under the Apache 2.0 License.