3 Go SDK
Insidious Fiddler edited this page 2026-02-06 21:04:12 +00:00

Go SDK

The native Go SDK for Hoist with real-time streaming support.

Installation

go get git.macco.dev/macco/hoist/sdks/go/hoist

Quick Start

package main

import (
    "context"
    "log"

    "git.macco.dev/macco/hoist/sdks/go/hoist"
)

func main() {
    client, err := hoist.NewClient(hoist.Config{
        APIKey:  "hoist_live_xxx",
        BaseURL: "https://hoist.example.com",
    })
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    ctx := context.Background()

    enabled, err := client.Bool(ctx, "new-feature", false, hoist.Context{
        TargetingKey: "user-123",
    })
    if err != nil {
        log.Printf("evaluation error: %v", err)
    }

    if enabled {
        // New feature code path
    }
}

Configuration

client, err := hoist.NewClient(hoist.Config{
    // Required
    APIKey:  "hoist_live_xxx",
    BaseURL: "https://hoist.example.com",

    // Optional
    PollInterval: 30 * time.Second,  // Polling interval (if streaming disabled)
    HTTPClient:   &http.Client{},    // Custom HTTP client
    Logger:       myLogger,          // Custom logger

    // Callbacks
    OnReady: func() {
        log.Println("Hoist client ready")
    },
    OnError: func(err error) {
        log.Printf("Hoist error: %v", err)
    },
    OnFlagsUpdated: func(keys []string) {
        log.Printf("Flags updated: %v", keys)
    },
})

API Key Types

Server Keys (hoist_sk_*)

Server keys provide full functionality:

  • Fetches complete flag configuration on startup
  • Evaluates all flags locally (fast, no network latency)
  • Receives real-time updates via WebSocket streaming
  • Thread-safe for concurrent use
client, _ := hoist.NewClient(hoist.Config{
    APIKey: "hoist_sk_live_xxx",  // Server key
})

Client Keys (hoist_pk_*)

Client keys are for untrusted environments:

  • Evaluates flags via API calls (protects targeting rules)
  • No local configuration storage
  • Suitable for mobile apps or browser-exposed code
client, _ := hoist.NewClient(hoist.Config{
    APIKey: "hoist_pk_xxx",  // Client key
})

Evaluation Methods

Boolean Flags

// Simple evaluation
enabled, err := client.Bool(ctx, "dark-mode", false, hoist.Context{
    TargetingKey: "user-123",
})

// With detailed result
result := client.BoolDetail(ctx, "dark-mode", false, hoist.Context{
    TargetingKey: "user-123",
})
fmt.Printf("Value: %v, Reason: %s, Variant: %s\n",
    result.Value, result.Reason, result.VariantKey)

String Flags

version, err := client.String(ctx, "api-version", "v1", hoist.Context{
    TargetingKey: "user-123",
})

// With details
result := client.StringDetail(ctx, "api-version", "v1", hoist.Context{
    TargetingKey: "user-123",
})

Integer Flags

limit, err := client.Int(ctx, "rate-limit", 100, hoist.Context{
    TargetingKey: "user-123",
})

// With details
result := client.IntDetail(ctx, "rate-limit", 100, hoist.Context{
    TargetingKey: "user-123",
})

Float Flags

threshold, err := client.Float(ctx, "score-threshold", 0.5, hoist.Context{
    TargetingKey: "user-123",
})

// With details
result := client.FloatDetail(ctx, "score-threshold", 0.5, hoist.Context{
    TargetingKey: "user-123",
})

JSON Flags

type FeatureConfig struct {
    MaxItems  int      `json:"maxItems"`
    Enabled   bool     `json:"enabled"`
    Allowlist []string `json:"allowlist"`
}

var config FeatureConfig
err := client.JSON(ctx, "feature-config", FeatureConfig{MaxItems: 10}, &config, hoist.Context{
    TargetingKey: "user-123",
})

// With details
result := client.JSONDetail(ctx, "feature-config", FeatureConfig{MaxItems: 10}, hoist.Context{
    TargetingKey: "user-123",
})

Evaluation Context

Provide user context for targeting:

evalCtx := hoist.Context{
    // Required for consistent targeting and percentage rollouts
    TargetingKey: "user-123",

    // Optional attributes for targeting rules
    Attributes: map[string]any{
        "email":        "user@example.com",
        "plan":         "enterprise",
        "country":      "US",
        "beta_tester":  true,
        "account_age":  365,
        "teams":        []string{"engineering", "platform"},
    },
}

enabled, err := client.Bool(ctx, "feature", false, evalCtx)

Evaluation Results

Detailed results include:

result := client.BoolDetail(ctx, "feature", false, evalCtx)

result.Value       // The evaluated value
result.VariantKey  // The variant key (if multivariate)
result.Reason      // Why this value was returned
result.RuleID      // Which rule matched
result.SegmentKey  // Which segment matched (if any)
result.Version     // Flag configuration version
result.Error       // Evaluation error (if any)

Evaluation Reasons

Reason Description
ReasonDefault No rules matched, using default
ReasonTargetingMatch Matched a targeting rule
ReasonSegmentMatch Matched via segment
ReasonRollout Percentage rollout
ReasonIdentityOverride User-specific override
ReasonDisabled Flag is disabled
ReasonNotFound Flag doesn't exist
ReasonError Evaluation error

Hooks

Add hooks for observability:

client.AddHook(hoist.Hook{
    Before: func(ctx context.Context, key string, evalCtx hoist.Context) {
        log.Printf("Evaluating flag: %s for user: %s", key, evalCtx.TargetingKey)
    },
    After: func(ctx context.Context, key string, result hoist.EvaluationResult) {
        // Log to your metrics system
        metrics.RecordFlagEvaluation(key, result.Reason, result.Value)
    },
    Error: func(ctx context.Context, key string, err error) {
        log.Printf("Flag evaluation error: %s: %v", key, err)
    },
})

Client Methods

Ready Check

if client.Ready() {
    // Client has loaded configuration
}

Force Refresh

err := client.Refresh(ctx)

List All Flags

keys := client.AllFlags()
for _, key := range keys {
    fmt.Println(key)
}

Close Client

client.Close()  // Stops streaming/polling, releases resources

Thread Safety

The Hoist client is fully thread-safe:

var client *hoist.Client

func init() {
    var err error
    client, err = hoist.NewClient(hoist.Config{
        APIKey:  "hoist_live_xxx",
        BaseURL: "https://hoist.example.com",
    })
    if err != nil {
        log.Fatal(err)
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    // Safe to call from multiple goroutines
    enabled, _ := client.Bool(r.Context(), "feature", false, hoist.Context{
        TargetingKey: getUserID(r),
    })
    // ...
}

Error Handling

enabled, err := client.Bool(ctx, "feature", false, evalCtx)
if err != nil {
    // Log the error but use the default value
    log.Printf("flag evaluation error: %v", err)
    // enabled is already set to the default (false)
}

// Use the value (default if error occurred)
if enabled {
    // ...
}

Custom Logger

Implement the Logger interface:

type Logger interface {
    Debug(msg string, args ...any)
    Info(msg string, args ...any)
    Warn(msg string, args ...any)
    Error(msg string, args ...any)
}

// Example with zerolog
type ZerologAdapter struct {
    log zerolog.Logger
}

func (l ZerologAdapter) Debug(msg string, args ...any) {
    l.log.Debug().Fields(toMap(args)).Msg(msg)
}
// ... implement other methods

client, _ := hoist.NewClient(hoist.Config{
    Logger: ZerologAdapter{log: zerolog.New(os.Stdout)},
})

Best Practices

1. Initialize Once

Create one client and reuse it:

// Good: Single client instance
var flagClient *hoist.Client

func main() {
    flagClient, _ = hoist.NewClient(config)
    defer flagClient.Close()
    // ... use flagClient throughout your app
}

// Bad: Creating clients per request
func handler(w http.ResponseWriter, r *http.Request) {
    client, _ := hoist.NewClient(config)  // Don't do this!
    defer client.Close()
}

2. Always Provide Targeting Key

// Good: Consistent targeting
client.Bool(ctx, "feature", false, hoist.Context{
    TargetingKey: userID,
})

// Bad: No targeting key (percentage rollouts won't be sticky)
client.Bool(ctx, "feature", false, hoist.Context{})

3. Handle Errors Gracefully

enabled, err := client.Bool(ctx, "feature", false, evalCtx)
if err != nil {
    // Log but continue with default
    log.Printf("flag error: %v", err)
}
// Always use the returned value (it's the default on error)

4. Use Meaningful Defaults

// Good: Safe default that doesn't break existing behavior
enabled, _ := client.Bool(ctx, "new-experimental-feature", false, evalCtx)

// Be careful: Default true means all users get the feature on error
enabled, _ := client.Bool(ctx, "critical-feature", true, evalCtx)

Next Steps