Shipeasy

Go

The Shipeasy Go server SDK — context-aware client, local evaluation, configs, experiments, and metric tracking.

Production readyOn this page · 4 min readUpdated · June 18, 2026Works with · Go 1.21+

Server-side flags, configs, experiments, and tracking for Go. Evaluates locally against a background-polled blob — see the shared evaluation model.

Install

go get github.com/shipeasy-ai/sdk-go

Configure

Initialise once at startup with Configure, passing the server key plus an optional Attributes transform that maps your user object onto a shipeasy.User. This does the first fetch and starts the background poll.

import (
    "os"

    shipeasy "github.com/shipeasy-ai/sdk-go"
)

shipeasy.Configure(shipeasy.Options{
    APIKey: os.Getenv("SHIPEASY_SERVER_KEY"),
    Attributes: func(u any) shipeasy.User {
        user := u.(*User)
        return shipeasy.User{"user_id": user.ID, "plan": user.Plan}
    },
})

Bind a client to the user

NewClient now takes the user object — the Attributes transform turns it into the evaluation context, so the getters take no user argument. It panics if Configure was not called first.

flags := shipeasy.NewClient(currentUser)

Evaluate a feature flag

if flags.GetFlag("new_checkout") {
    // ship it
}

Go has no default arguments, so use the …Or variant for a fallback. It's returned only when the flag can't be evaluated (client not ready or gate absent) — a gate that evaluates false returns false:

on := flags.GetFlagOr("new_checkout", true)

For the value plus the reason, use GetFlagDetail — see Evaluation reasons:

d := flags.GetFlagDetail("new_checkout")
// d.Value bool, d.Reason e.g. shipeasy.ReasonRuleMatch

Read a dynamic config

cfg, ok := flags.GetConfig("billing_copy")
copy := flags.GetConfigOr("billing_copy", map[string]any{"cta": "Buy"})

Resolve an experiment

r := flags.GetExperiment("checkout_button", map[string]any{"color": "blue"})

if r.InExperiment {
    _ = r.Group  // e.g. "treatment"
    _ = r.Params // map[string]any
}

Track an event

flags.Track("purchase", map[string]any{"amount": 49})

The low-level Engine

The per-call style still works for back-compat — construct an Engine directly with NewEngine and pass the user on every call:

engine := shipeasy.NewEngine(shipeasy.Options{APIKey: os.Getenv("SHIPEASY_SERVER_KEY")})
if err := engine.Init(context.Background()); err != nil {
    panic(err)
}
defer engine.Destroy()

engine.GetFlag("new_checkout", shipeasy.User{"user_id": "u_123"})

Anonymous visitors

shipeasy.Middleware mints the shared __se_anon_id cookie for any request without one; read it with shipeasy.AnonID(r) so the server buckets identically to the browser:

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

func handler(w http.ResponseWriter, r *http.Request) {
    flags := shipeasy.NewClient(anonUser(shipeasy.AnonID(r)))
    if flags.GetFlag("new_checkout") { /* ... */ }
}

Testing

NewTestEngine does zero network — no key, Init/Track are no-ops. Seed with Override* and reset with ClearOverrides. See Testing.

engine := shipeasy.NewTestEngine()
engine.OverrideFlag("new_checkout", true)
engine.GetFlag("new_checkout", shipeasy.User{"user_id": "u_123"}) // true
engine.ClearOverrides()

Errors & feedback

Report a handled exception with See() so it folds into the errors primitive with a one-sentence consequence — what feature broke and how it degraded. See() rides the same Configure() boot (server key); there is no separate error SDK or second key.

if err := chargeCard(order, prices); err != nil {
    shipeasy.See(err).CausesThe("checkout").To("use cached prices").
        Extras(map[string]any{"order_id": order.ID})
}

Use shipeasy.SeeViolation("large query") for a non-exception problem. Full grammar — consequence phrasing, control-flow exceptions, anti-patterns — is in Error reporting with see().

The in-app bug & feature report overlay is a standalone <script> tag you drop into your frontend — platform-agnostic, no server SDK required. See The devtools overlay.

Was this page helpful?
✎ Edit this page

On this page