Skip to main content

Go SDK

The Cuttlegate Go SDK evaluates feature flags in your Go services. It requires Go 1.24 or later.

Install

go get github.com/The127/cuttlegate/sdk/go

Quick start

package main

import (
"context"
"fmt"
"log"

cuttlegate "github.com/The127/cuttlegate/sdk/go"
)

func main() {
client, err := cuttlegate.NewClient(cuttlegate.Config{
BaseURL: "https://flags.example.com",
ServiceToken: "cg_your_api_key_here", // API key from the Cuttlegate UI
Project: "my-project",
Environment: "production",
})
if err != nil {
log.Fatal(err)
}

ctx := context.Background()
evalCtx := cuttlegate.EvalContext{
UserID: "user-123",
Attributes: map[string]any{"plan": "pro"},
}

// Bool is the simplest way to evaluate a boolean flag
enabled, err := client.Bool(ctx, "dark-mode", evalCtx)
if err != nil {
log.Fatal(err)
}
fmt.Println("dark-mode enabled:", enabled)
}

NewClient validates the configuration and returns an error if any required field is missing. No network calls are made at construction time.

Evaluation methods

MethodReturnsNotes
Bool(ctx, key, evalCtx)(bool, error)Preferred for boolean flags
String(ctx, key, evalCtx)(string, error)Returns the variant string
Evaluate(ctx, key, evalCtx)(EvalResult, error)Returns full result including Reason and Variant
EvaluateAll(ctx, evalCtx)(map[string]EvalResult, error)Evaluates all flags in one HTTP round trip
EvaluateFlag(ctx, key, evalCtx)(FlagResult, error)Deprecated — use Bool or Evaluate for new code
Subscribe(ctx, key)(<-chan FlagUpdate, <-chan error, error)Real-time SSE stream of flag state changes

EvaluateAll is the most efficient method when you need multiple flags — one HTTP request regardless of how many flags exist.

EvalResult fields

type EvalResult struct {
Key string // flag key
Enabled bool // whether the flag is enabled for this context
Value string // Deprecated — use Variant instead
Variant string // Primary field: variant key; "true"/"false" for bool flags
Reason string // "rule_match", "default", "disabled", or "percentage_rollout"
EvaluatedAt string // ISO 8601 timestamp
}

Typed errors

All methods return typed errors — no string-only errors.

results, err := client.EvaluateAll(ctx, evalCtx)
if err != nil {
var authErr *cuttlegate.AuthError
var notFoundErr *cuttlegate.NotFoundError
var serverErr *cuttlegate.ServerError

switch {
case errors.As(err, &authErr):
log.Printf("auth failed: %d %s", authErr.StatusCode, authErr.Message)
case errors.As(err, &notFoundErr):
log.Printf("%s %q not found", notFoundErr.Resource, notFoundErr.Key)
case errors.As(err, &serverErr):
log.Printf("server error: %d", serverErr.StatusCode)
default:
log.Printf("unexpected error: %v", err)
}
}

Real-time streaming

Subscribe opens an SSE connection and delivers flag state changes as they happen:

updates, errs, err := client.Subscribe(ctx, "dark-mode")
if err != nil {
log.Fatal(err)
}

for {
select {
case update := <-updates:
fmt.Printf("flag %s changed: enabled=%v at %s\n",
update.Key, update.Enabled, update.UpdatedAt)
case err := <-errs:
log.Printf("stream error: %v", err)
case <-ctx.Done():
return
}
}

Both channels are closed when ctx is cancelled. A terminal AuthError is delivered on the error channel before it closes on 401/403.

Testing without a live server

Use the cuttlegatetesting subpackage to mock flags in-process:

import (
cgt "github.com/The127/cuttlegate/sdk/go/testing"
)

func TestMyService(t *testing.T) {
mock := cgt.NewMockClient()
mock.Enable("dark-mode")
mock.SetVariant("banner-text", "holiday")

svc := mypackage.NewService(mock) // inject as cuttlegate.Client
svc.GetFeatures(context.Background(), "user-123")

if err := mock.AssertEvaluated("dark-mode"); err != nil {
t.Error(err)
}
}

Available mock helpers: Enable, Disable, SetVariant, AssertEvaluated, AssertNotEvaluated, Reset.

CachedClient

For hot paths, use CachedClient. It seeds an in-memory cache via EvaluateAll on Bootstrap and keeps it fresh via a background SSE connection:

cc, err := cuttlegate.NewCachedClient(cuttlegate.Config{
BaseURL: "https://flags.example.com",
ServiceToken: "cg_your_api_key_here",
Project: "my-project",
Environment: "production",
})
if err != nil {
log.Fatal(err)
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

if err := cc.Bootstrap(ctx, cuttlegate.EvalContext{UserID: "user-123"}); err != nil {
log.Fatal(err)
}

// Cache hit — no HTTP call
enabled, _ := cc.Bool(ctx, "dark-mode", cuttlegate.EvalContext{UserID: "user-123"})

CachedClient satisfies the Client interface — you can substitute it transparently.

Offline persistence (FlagStore)

By default, the cache lives only in memory. If the server is down when Bootstrap is called, it fails. To survive restarts, provide a FlagStore implementation:

type FlagStore interface {
Save(ctx context.Context, flags map[string]EvalResult) error
Load(ctx context.Context) (map[string]EvalResult, error)
}

The SDK ships with NoopFlagStore (the default — no persistence). You bring your own:

cc, _ := cuttlegate.NewCachedClient(cuttlegate.Config{
BaseURL: "https://flags.example.com",
ServiceToken: "cg_your_api_key_here",
Project: "my-project",
Environment: "production",
Store: myRedisStore, // or file store, sqlite, etc.
})

When Store is set:

  • Save is called after successful bootstrap and on every SSE update.
  • Load is called when bootstrap fails (server unreachable). If it returns a non-empty map, the cache is seeded from it and the SSE goroutine starts normally.
  • Auth errors (401/403) never fall back to the store.

Configuration reference

FieldTypeRequiredDefaultDescription
BaseURLstringyesBase URL of the Cuttlegate server
ServiceTokenstringyesAPI key from the Cuttlegate UI (cg_...)
ProjectstringyesProject slug
EnvironmentstringyesEnvironment slug (e.g. "production")
HTTPClient*http.ClientnonilCustom HTTP client; if set, Timeout is ignored
StreamHTTPClient*http.ClientnonilCustom HTTP client for SSE connections; must not have a short timeout
Timeouttime.Durationno10sRequest timeout for evaluation calls when HTTPClient is nil
Defaultsmap[string]FlagDefaultnonilFallback flag values when the server is unreachable
StoreFlagStorenoNoopFlagStore{}Persistence store for offline bootstrap (see above)

Custom HTTP client

Pass your own *http.Client if you have existing transport configuration (TLS, proxies, retry middleware):

client, err := cuttlegate.NewClient(cuttlegate.Config{
BaseURL: "https://flags.example.com",
ServiceToken: "cg_your_api_key_here",
Project: "my-project",
Environment: "production",
HTTPClient: myConfiguredHTTPClient,
})

If HTTPClient is nil, a default client with a 10-second timeout is used. The Timeout field only applies when HTTPClient is nil.