SDK

Go

The official ShipSilently client for Go. Generic evaluation, first-class context.Context support, SSE streaming with automatic reconnection, and a last-known-good cache.

View source on GitHub — shipsilently/shipsilently-go

Install

terminal
go get github.com/shipsilently/shipsilently-go

Quick start

main.go
package main

import (
	"context"
	"fmt"
	"os"

	shipsilently "github.com/shipsilently/shipsilently-go"
)

func main() {
	client := shipsilently.New(shipsilently.Config{
		APIKey: os.Getenv("SHIPSILENTLY_KEY"),
		// APIURL defaults to https://api.shipsilently.com
	})

	ctx := context.Background()
	userCtx := shipsilently.UserContext{
		"userId":  "u_123",
		"plan":    "pro",
		"country": "US",
	}

	// Evaluate is a generic package-level function, not a method — Go does not
	// allow type parameters on methods. The default value fixes T, so
	// useNewFlow is a bool and variant is a string, with no casting.
	useNewFlow, err := shipsilently.Evaluate(ctx, client, "new-checkout-v2", userCtx, false)
	if err != nil {
		// err is non-nil on a failed lookup, but the returned value is still
		// safe to use: it is the last-known-good cached value, or your default.
		fmt.Fprintf(os.Stderr, "flag lookup degraded: %v\n", err)
	}

	variant, _ := shipsilently.Evaluate(ctx, client, "hero-variant", userCtx, "control")

	fmt.Println(useNewFlow, variant)
}
Evaluate is a package-level function, not a method. Go does not allow type parameters on methods, so generic evaluation has to take the client as an argument: shipsilently.Evaluate(ctx, client, key, userCtx, default). The default value fixes T, so there is no casting.

Errors and the cache

Evaluate returns (value, error), and the value is always safe to use. On a failed request the client serves the last-known-good cached value with a nil error. A non-nil error therefore means there was no cached value either, and what you got back is the default you passed.

handling
enabled, err := shipsilently.Evaluate(ctx, client, "new-checkout", userCtx, false)
if err != nil {
    // No cached value was available — 'enabled' is the compile-time default.
    // Usually worth a metric, rarely worth failing the request.
    log.Printf("shipsilently degraded: %v", err)
}
// Use 'enabled' either way.

Set DisableCache: true to opt out and have every failure surface an error with the compile-time default. See Resilience for the full failure matrix.

Evaluate all flags & stream

EvaluateAll fetches every flag in one call. Stream keeps them fresh over SSE and returns a cancel function.

main.go
package main

import (
	"context"
	"log"
	"os"
	"time"

	shipsilently "github.com/shipsilently/shipsilently-go"
)

func main() {
	client := shipsilently.New(shipsilently.Config{
		APIKey: os.Getenv("SHIPSILENTLY_KEY"),

		// Reconnection backoff. Zero values use the defaults shown here.
		Retry: shipsilently.RetryConfig{
			BaseDelay:        1 * time.Second,
			MaxDelay:         60 * time.Second,
			HeartbeatTimeout: 90 * time.Second,
		},
		// Refresh cadence when streaming is plan-gated (HTTP 402).
		PollingInterval: 30 * time.Second,
	})

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

	// Fetch everything once for the process-level context.
	all, err := client.EvaluateAll(ctx, shipsilently.UserContext{"userId": "system"})
	if err != nil {
		log.Printf("initial load degraded: %v", err)
	}
	log.Printf("loaded %d flags", len(all))

	// Then keep it fresh. cancel() closes the stream.
	cancel, err := client.Stream(ctx, shipsilently.UserContext{"userId": "system"},
		func(flags map[string]any) {
			log.Printf("flags refreshed: %d", len(flags))
		})
	if err != nil {
		log.Fatalf("stream: %v", err)
	}
	defer cancel()

	select {}
}

Configuration

FieldTypeDefaultNotes
APIKeystringRequired. Environment-scoped key.
APIURLstringhttps://api.shipsilently.comOverride for self-hosted deployments.
HTTP*http.Client5s timeoutBring your own transport, proxy, or tracing.
RetryRetryConfig1s / 60s / 90sBase delay, max delay, heartbeat timeout.
PollingIntervaltime.Duration30sCadence when streaming is plan-gated (HTTP 402).
DisableCacheboolfalseTurn off last-known-good caching.
Logger*log.Loggerlog.Default()Receives connection warnings, deduplicated to one per minute per class.

API reference

SignatureNotes
New(cfg Config) *ClientConstruct once and share; safe for concurrent use.
Evaluate[T](ctx, c, key, userCtx, default) (T, error)Package-level generic function.
(*Client) EvaluateAll(ctx, userCtx) (map[string]any, error)Every flag in one call.
(*Client) Stream(ctx, userCtx, handler) (context.CancelFunc, error)Call the returned func to close the stream.

Types

types.go
// UserContext holds attributes used for flag targeting.
type UserContext map[string]any

// StreamHandler is called with the full flag map on every update.
type StreamHandler func(flags map[string]any)
Concurrency. A *Client is safe for concurrent use, its cache is guarded by a read-write mutex. Create one per process, not per request.