Skip to main content

Go

A server-side Go SDK for Flagpole feature flags — environment targeting, net/http middleware, and a zero-dependency core.

Updates are by polling, not streaming

Unlike the other Flagpole SDKs, the Go SDK does not hold a WebSocket. It refreshes the flag set by calling the REST API on an interval (WithPollInterval, default 30 seconds). A flag change therefore takes up to one poll interval to take effect in your app.

Lower the interval for faster propagation, or call client.Refresh(ctx) after a known change:

flagpole.WithPollInterval(5 * time.Second)

Setting the interval to 0 disables polling entirely — the flag set is then only what Start loaded plus any manual Refresh.

Table of Contents​

Features​

  • 🔁 Polling: configurable-interval REST refresh (see the note above)
  • ⚡ Non-blocking reads: after Start, IsEnabled is a lock-guarded in-memory lookup with no network I/O
  • 🌍 Environment Targeting: load and evaluate flags per environment
  • 🧩 net/http middleware: works with chi, gorilla/mux, and the stdlib mux
  • 📦 Zero dependencies: standard library only
  • 🔐 Fail-safe: unknown or unreachable flags return your configured fallback

Installation​

go get github.com/flagpole-corp/flagpole-sdks/packages/server/go
import flagpole "github.com/flagpole-corp/flagpole-sdks/packages/server/go"

Requirements​

  • Go 1.21+

Quick Start​

1. Create and start the client​

client, err := flagpole.NewClient(
"fp_live_your_api_key",
flagpole.WithEnvironments("production"),
flagpole.WithPollInterval(15*time.Second),
flagpole.WithFallbacks(map[string]bool{"new-checkout": false}),
)
if err != nil {
log.Fatal(err)
}
if err := client.Start(context.Background()); err != nil {
log.Fatal(err)
}
defer client.Close()

Start blocks on the first fetch (bounded by the context) and then polls in the background.

2. Check flags​

if client.IsEnabled("new-checkout") {
// ...
}

flag, ok := client.Flag("new-checkout") // the full flag + whether it was found
all := client.AllFlags() // map[string]FeatureFlag (a copy)

API Reference​

NewClient(apiKey string, opts ...Option) (*Client, error)​

Constructs a client. Performs no I/O.

Methods​

MethodDescription
Start(ctx) errorInitial fetch, then start polling. Re-fetches if called again.
Refresh(ctx) errorRe-fetch every flag now
IsEnabled(name) boolFallback for unknown / disabled / non-matching flags. Never panics.
Flag(name) (FeatureFlag, bool)The full flag and whether it was found
AllFlags() map[string]FeatureFlagA copy of every loaded flag
Started() boolWhether Start has completed at least once
Close() errorStop the poller (safe to call repeatedly)

Options​

OptionDescription
WithEnvironment(Environment)Which backend — Development / Staging / Production (default)
WithEnvironments(...string)Flag environments to load. Default: all.
WithPollInterval(time.Duration)Refresh frequency. Default 30s. 0 disables polling.
WithTimeout(time.Duration)Per-request timeout. Default 10s.
WithFallbacks(map[string]bool)Value for a flag that isn't loaded
WithLogger(*slog.Logger)Where the SDK logs. Default: discard.
WithHTTPClient(*http.Client)Bring your own HTTP client

FeatureFlag​

type FeatureFlag struct {
ID string
Name string
Description string
IsEnabled bool // raw switch — prefer client.IsEnabled()
Project string
Organization string
Conditions map[string]any
Environments []string // empty means "all"
CreatedAt time.Time
UpdatedAt time.Time
}

net/http Middleware​

mux := http.NewServeMux()

// 404 unless the flag is on (pass a status to override)
mux.Handle("/beta", client.RequireFlag("beta-access")(betaHandler))

// attach the client to the request context
mux.Handle("/dashboard", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, _ := flagpole.FromContext(r.Context())
fmt.Fprintf(w, "advanced=%v", c.IsEnabled("advanced-dashboard"))
}))

http.ListenAndServe(":8080", client.Middleware(mux))

Client.Middleware and Client.RequireFlag return standard func(http.Handler) http.Handler values, so they compose with chi, gorilla/mux, alice, and the stdlib mux.

Configuration​

Environments​

WithEnvironments is a filter — it controls which flags are loaded and how IsEnabled evaluates targeting. A flag with no environments always applies.

Backend host​

EnvironmentREST
flagpole.Developmenthttp://localhost:5000
flagpole.Staginghttps://api.staging.useflagpole.dev
flagpole.Productionhttps://useflagpole-api.onrender.com
flagpole.NewClient(key, flagpole.WithEnvironment(flagpole.Staging))

Logging​

The SDK discards logs by default. Pass a *slog.Logger to see them:

flagpole.WithLogger(slog.New(slog.NewJSONHandler(os.Stdout, nil)))

Error Handling​

IsEnabled returns the configured fallback (default false) for any flag it can't resolve and never panics.

Start and Refresh do return an error if the fetch fails:

var apiErr *flagpole.APIError
if err := client.Start(ctx); errors.As(err, &apiErr) {
log.Printf("Flagpole unavailable: %d %s", apiErr.StatusCode, apiErr.Body)
// carry on — IsEnabled will use fallbacks
}

A failed poll is logged (via the configured logger) and retried on the next tick; it does not stop the client.

Best Practices​

1. One client per process​

Create the client at startup, share it, Close() on shutdown.

2. Start before serving traffic​

Call Start(ctx) in your bootstrap so the first request already has flags.

3. Tune the poll interval to your needs​

30s is fine for most rollouts. Drop it to a few seconds for kill-switch-style flags, or call Refresh from an admin webhook.

4. Always set fallbacks for critical flags​

flagpole.WithFallbacks(map[string]bool{"payments-v2": false})

Troubleshooting​

Flags are stale​

That's expected up to one poll interval. Lower WithPollInterval or call Refresh.

Flags are always the fallback value​

  • Wrong or expired API key
  • The flag targets environments not in your WithEnvironments list
  • The first fetch failed — check the error from Start

Start returns an *APIError​

The API rejected the request (StatusCode 401 → bad key). The client is still usable; reads fall back until the next successful fetch.

Contributing​

The SDK lives in the flagpole-sdks monorepo under packages/server/go.

git clone https://github.com/flagpole-corp/flagpole-sdks.git
cd flagpole-sdks/packages/server/go

gofmt -l .
go vet ./...
go test ./...

License​

MIT

Support​