[go: up one dir, main page]

Skip to main content

GrowthBook Go SDK

Go SDK Resources
v0.2.4
growthbook-golangGo ModulesGo example appGet help on Slack

Requirements

  • Go version 1.21 or higher (tested with 1.21, 1.22, and 1.23)

Installation

go get github.com/growthbook/growthbook-golang

Quick Usage

import (
"context"
"log"
gb "github.com/growthbook/growthbook-golang"
)

// Create a new client instance with a client key and a data source that loads features
// in the background using an SSE stream. Pass the client's options to the NewClient function.
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-XXXX"),
gb.WithSseDataSource(),
)
defer client.Close()

if err != nil {
log.Fatal("Client initialization failed: ", err)
}

// The data source starts asynchronously. Use EnsureLoaded to wait until the client data
// is initialized for the first time.
if err := client.EnsureLoaded(context.Background()); err != nil {
log.Fatal("Data loading failed: ", err)
}

// Create a child client with specific attributes.
attrs := gb.Attributes{"id": 100, "user": "user1"}
child, err := client.WithAttributes(attrs)
if err != nil {
log.Fatal("Child client creation failed: ", err)
}

// Evaluate a text feature.
buttonColor := child.EvalFeature(context.Background(), "buy-button-color")
if buttonColor.Value == "blue" {
// Perform actions for blue button.
}

// Evaluate a boolean feature.
darkMode := child.EvalFeature(context.Background(), "dark-mode")
if darkMode.On {
// Enable dark mode.
}

Client

The client is the core component of the GrowthBook SDK. After installing and importing the SDK, create a single shared instance of growthbook.Client using the growthbook.NewClient function with a list of options. You can customize the client with options such as a custom logger, client key, decryption key, default attributes, or a feature list loaded from JSON. The client is thread-safe and can be safely used from multiple goroutines.

While you can evaluate features directly using the main client instance, it's recommended to create child client instances that include session- or query-specific data. To create a child client with local attributes, call client.WithAttributes:

attrs := gb.Attributes{"id": 100, "user": "Bob"}
child, err := client.WithAttributes(attrs)

You can then evaluate features using the child client:

res := child.EvalFeature(context.Background(), "main-button-color")

Additional options, such as WithLogger, WithUrl, and WithAttributesOverrides, can also be used to customize child clients. Since child clients share data with the main client instance, they will automatically receive feature updates.

To stop background updates, call client.Close() on the main client instance when it is no longer needed.

Additional options for sticky bucketing:

  • WithStickyBucketService: Provides a service implementation for storing and retrieving sticky bucket assignments
  • WithStickyBucketAttributes: Sets specific attributes to use for sticky bucketing (if different from regular attributes)
// Configure a client with sticky bucketing
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-XXXX"),
gb.WithStickyBucketService(NewInMemoryStickyBucketService()),
)

Using Features

The primary method, client.EvalFeature(ctx, key), accepts a feature key and uses the stored feature definitions and attributes to evaluate the feature value. It returns a FeatureResult value that includes detailed information about why the value was assigned to the user:

  • Value: The JSON value of the feature (or nil if not defined), represented as a FeatureValue (an alias for interface{}, using Go's default behavior for JSON).
  • On and Off: The JSON value cast as booleans (to make your code easier to read).
  • Source: A value of type FeatureResultSource that explains why the value was assigned to the user. Possible values include UnknownFeatureResultSource, DefaultValueResultSource, ForceResultSource, or ExperimentResultSource.
  • Experiment: Information about the experiment (if any) used to assign the value.
  • ExperimentResult: The result of the experiment (if any) that determined the value.

Here's an example that uses all of these fields:

result, err := client.EvalFeature(context.TODO(), "my-feature")
if err != nil {
// Handle the error
}

// The JSON value (which may be null, a string, boolean, number, array, or object).
fmt.Println(result.Value)

if result.On {
// The feature value is truthy (in a JavaScript sense).
}
if result.Off {
// The feature value is falsy.
}

// If the feature value was assigned as part of an experiment:
if result.Source == gb.ExperimentResultSource {
// Get all the possible variations that could have been assigned.
fmt.Println(result.Experiment.Variations)
}

Loading Features and Experiments

For the GrowthBook SDK to function, it requires feature and experiment definitions from the GrowthBook API. There are several ways to provide this data to the SDK.

Automatic Features Refresh

The Go SDK provides multiple mechanisms for automatically keeping your feature definitions up to date.

Server-Sent Events (SSE)

SSE provides real-time feature updates with minimal overhead. When you use WithSseDataSource(), the SDK establishes a persistent connection to receive live updates whenever features change.

// Create a client with SSE for real-time updates
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithSseDataSource(), // Enable SSE streaming
)
if err != nil {
log.Fatal("Failed to create client:", err)
}
defer client.Close()

// Wait for initial feature load
if err := client.EnsureLoaded(context.Background()); err != nil {
log.Fatal("Failed to load features:", err)
}

// Features will now update automatically via SSE

Benefits of SSE:

  • Real-time updates without polling overhead
  • Lower latency for feature changes
  • Reduced server load
  • Automatic reconnection on connection loss

Polling Data Source

For environments where SSE isn't supported, use polling to periodically fetch feature updates:

import "time"

// Poll for updates every 30 seconds
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithPollDatasource(30 * time.Second), // Poll every 30 seconds
)
if err != nil {
log.Fatal("Failed to create client:", err)
}
defer client.Close()

// Wait for initial feature load
if err := client.EnsureLoaded(context.Background()); err != nil {
log.Fatal("Failed to load features:", err)
}

When to Use Polling:

  • SSE is blocked by firewalls or proxies
  • Running in restricted network environments
  • Need predictable update intervals
  • Simpler infrastructure requirements

Choosing Between SSE and Polling

FeatureSSEPolling
LatencyReal-time (milliseconds)Based on interval (seconds/minutes)
Server LoadLow (one connection)Higher (periodic requests)
Network RequirementsPersistent connectionIntermittent requests
Use CaseProduction apps, real-time needsRestricted networks, batch updates

Built-in Fetching and Caching

The loading of features is an asynchronous process so that your app is not blocked while waiting, and it can continue its initialization. If you need to ensure that the definitions are loaded, use the client.EnsureLoaded call. This will block until the loading process finishes and will return an error if any failures occur.

// Create client with automatic updates
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithSseDataSource(),
)
if err != nil {
log.Fatal(err)
}

// EnsureLoaded blocks until features are ready
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := client.EnsureLoaded(ctx); err != nil {
log.Fatal("Features not loaded in time:", err)
}

// Features are now loaded and will update automatically
fmt.Println("Features loaded successfully")

Key Points:

  • EnsureLoaded is thread-safe and can be called from multiple goroutines simultaneously
  • Both NewClient and EnsureLoaded respect the contexts passed to them
  • The features cache is shared among all child client instances created via client.WithXXX calls
  • Use context timeouts to prevent indefinite blocking

Custom Integration

Feature definitions are stored in the client's shared data. Normally, the data source will download them from the GrowthBook site, but you can provide an initial set during the NewClient call using the WithFeatures, WithJsonFeatures, or WithEncryptedJsonFeature options.

// Load features from a local JSON file
featuresJSON, err := os.ReadFile("features.json")
if err != nil {
log.Fatal(err)
}

client, err := gb.NewClient(
context.Background(),
gb.WithJsonFeatures(featuresJSON),
)
if err != nil {
log.Fatal(err)
}

It is also possible to update the shared feature definitions using the SetXXXFeatures client methods:

// Update features dynamically
newFeaturesJSON := []byte(`{"features": {...}}`)
if err := client.UpdateFromApiResponseJSON(newFeaturesJSON); err != nil {
log.Error("Failed to update features:", err)
}

Manual Feature Updates

You can manually trigger feature updates when needed:

// Force a feature refresh
client.RefreshFeatures(context.Background())

// Or update from a JSON response
apiResponse, err := fetchFeaturesFromAPI()
if err != nil {
log.Error("Failed to fetch features:", err)
return
}

if err := client.UpdateFromApiResponseJSON(apiResponse); err != nil {
log.Error("Failed to update features:", err)
}

Attributes

You can specify attributes about the current user and request. These attributes are used for the following purposes:

  • Feature targeting (for example, paid users receive one value, while free users receive another).
  • Assigning persistent variations in A/B tests (for example, a user with id "123" always gets variation B).

Attributes can be any JSON data type—boolean, float, string, array, or object—and are represented by the Attributes type, which is an alias for Go's generic map[string]interface{} type used for JSON objects. If you know the attributes upfront, you can pass them into NewClient using the WithAttributes option:

attrs := gb.Attributes{
"id": "123",
"loggedIn": true,
"deviceId": "abc123def456",
"company": "acme",
"paid": false,
"url": "/pricing",
"browser": "chrome",
"mobile": false,
"country": "US",
}

client, err := gb.NewClient(context.Background(),
gb.WithAttributes(attrs),
)

You can also create a child client instance with updated attributes using the WithAttributes method:

attrs := gb.Attributes{
"id": "100",
"mobile": true,
}

client, err := client.WithAttributes(attrs)

This will completely overwrite the existing attributes object with the values you provide. If you want to merge the new attributes with the existing ones instead, you can use the WithAttributeOverrides method:

client, err := client.WithAttributeOverrides(attrs)

This method updates only the fields provided in attrs, keeping the other fields from the original client instance.

Be aware that changing attributes may change the assigned feature values. This can be disorienting to users if not handled carefully. A common approach is to refresh attributes only on navigation, when the window is focused, or after a user performs a major action such as logging in.

Secure Attributes

When secure attribute hashing is enabled, all targeting conditions in the SDK payload referencing attributes with datatype secureString or secureString[] will be anonymized via SHA-256 hashing. This allows you to safely target users based on sensitive attributes. You must enable this feature in your SDK Connection for it to take effect.

If your SDK Connection has secure attribute hashing enabled, you will need to manually hash any secureString or secureString[] attributes that you pass into the GrowthBook SDK.

To hash an attribute, use Go's crypto/sha256 package to compute the SHA-256 hashed value of your attribute plus your organization's secure attribute salt.

import (
"crypto/sha256"
"encoding/hex"
gb "github.com/growthbook/growthbook-golang"
)

// Your secure attribute salt (set in Organization Settings)
salt := "f09jq3fij"

// Hashing a secureString attribute
userEmail := user.Email
hasher := sha256.New()
hasher.Write([]byte(salt + userEmail))
hashedEmail := hex.EncodeToString(hasher.Sum(nil))

// Hashing a secureString[] attribute
userTags := user.Tags
hashedTags := make([]string, len(userTags))
for i, tag := range userTags {
hasher := sha256.New()
hasher.Write([]byte(salt + tag))
hashedTags[i] = hex.EncodeToString(hasher.Sum(nil))
}

// Create attributes with hashed values
attrs := gb.Attributes{
"id": user.ID,
"loggedIn": true,
"email": hashedEmail,
"tags": hashedTags,
}

// Create a client with the hashed attributes
client, err := client.WithAttributes(attrs)
if err != nil {
// Handle error
}

Custom Attributes

You can define custom attributes for advanced targeting scenarios beyond standard user properties:

// Standard and custom business attributes
attrs := gb.Attributes{
// Standard attributes
"id": user.ID,
"email": user.Email,
"country": user.Country,

// Custom business attributes
"lifetimeValue": user.LifetimeValue,
"subscriptionTier": user.Tier,
"accountAge": user.AccountAge,
"lastPurchaseDate": user.LastPurchase.Unix(),

// Computed attributes
"isHighValueCustomer": user.LifetimeValue > 1000,
"isPremium": user.Tier == "premium" || user.Tier == "enterprise",

// Array attributes for complex targeting
"purchasedCategories": user.Categories,
"enabledFeatures": user.Features,

// Request-specific attributes
"requestPath": req.URL.Path,
"userAgent": req.UserAgent(),
"clientIP": req.RemoteAddr,
}

client, err := client.WithAttributes(attrs)
if err != nil {
log.Error("Failed to set attributes:", err)
}

Best Practices for Custom Attributes:

  • Use consistent naming conventions across your application
  • Keep attribute values serializable (avoid complex nested structures)
  • Consider attribute cardinality for targeting rules
  • Document custom attributes in your team's documentation

Encrypted Features

The Go SDK supports encrypted feature payloads to protect sensitive feature configurations from being exposed in transit or at rest.

Setup Encrypted Features

  1. Enable encryption in your GrowthBook SDK Connection settings
  2. Copy the encryption key from the SDK Connection
  3. Configure the SDK with the decryption key:
import (
"context"
"log"
gb "github.com/growthbook/growthbook-golang"
)

// Create a client with encryption support
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithDecryptionKey("your_encryption_key_here"), // Add decryption key
gb.WithSseDataSource(),
)
if err != nil {
log.Fatal("Failed to create client:", err)
}
defer client.Close()

// Features are automatically decrypted when loaded
if err := client.EnsureLoaded(context.Background()); err != nil {
log.Fatal("Failed to load features:", err)
}

// Evaluate encrypted features normally
result := client.EvalFeature(context.Background(), "sensitive-feature")
fmt.Println("Feature value:", result.Value)

Loading Encrypted Features from JSON

You can also load encrypted features directly from JSON:

// Encrypted features JSON from your API
encryptedJSON := []byte(`{
"encryptedFeatures": "encrypted_payload_here...",
"status": 200
}`)

client, err := gb.NewClient(
context.Background(),
gb.WithDecryptionKey("your_encryption_key_here"),
gb.WithEncryptedJsonFeature(encryptedJSON),
)
if err != nil {
log.Fatal("Failed to create client:", err)
}

// Features are decrypted and ready to use
result := client.EvalFeature(context.Background(), "my-feature")

Security Best Practices

import (
"os"
gb "github.com/growthbook/growthbook-golang"
)

// Use environment variables for sensitive keys
decryptionKey := os.Getenv("GROWTHBOOK_DECRYPTION_KEY")
if decryptionKey == "" {
log.Fatal("GROWTHBOOK_DECRYPTION_KEY environment variable not set")
}

client, err := gb.NewClient(
context.Background(),
gb.WithClientKey(os.Getenv("GROWTHBOOK_CLIENT_KEY")),
gb.WithDecryptionKey(decryptionKey),
gb.WithSseDataSource(),
)

Security Recommendations:

  • Never hardcode encryption keys in source code
  • Use environment variables or secret management systems (HashiCorp Vault, AWS Secrets Manager)
  • Rotate keys regularly and update across all environments
  • Use different keys for different environments (dev, staging, production)

Handling Decryption Errors

client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithDecryptionKey("invalid-key"),
gb.WithSseDataSource(),
)
if err != nil {
log.Fatal("Client initialization failed:", err)
}

// EnsureLoaded will fail if decryption fails
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := client.EnsureLoaded(ctx); err != nil {
// Log error and potentially fall back to default values
log.Error("Failed to load encrypted features:", err)
// Use fallback logic or default feature values
}
Encryption Key Management

If the decryption key is incorrect or missing, the SDK will fail to load features. Ensure proper error handling and monitoring to detect decryption issues in production.

Sticky Bucketing

Sticky Bucketing ensures users see consistent experiment variations across sessions and devices. This is particularly useful when:

  • You need to slow down experiment enrollment without affecting existing users
  • You want to fix bugs in an experiment without including users who saw the buggy version
  • You need consistent experiences across different devices or sessions

Implementation

The SDK provides a built-in thread-safe in-memory implementation that you can use right away:

// Create an in-memory sticky bucket service
service := gb.NewInMemoryStickyBucketService()

// Create a client with sticky bucketing
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-XXXX"),
gb.WithStickyBucketService(service),
)

To use sticky bucketing in an experiment, set the BucketVersion and optionally MinBucketVersion properties:

exp := &gb.Experiment{
Key: "my-experiment",
Variations: []gb.FeatureValue{"control", "treatment"},
Meta: []gb.VariationMeta{
{Key: "0"}, // Use numeric keys to match variation IDs
{Key: "1"},
},
BucketVersion: 1, // Current version of the experiment
MinBucketVersion: 0, // Minimum version users must have seen
HashAttribute: "userId", // Primary attribute for bucketing
FallbackAttribute: "deviceId", // Used when primary is missing
}

result := client.RunExperiment(context.Background(), exp)

Custom Storage Implementation

Implement your own persistent storage by implementing the StickyBucketService interface:

type StickyBucketService interface {
GetAssignments(attributeName string, attributeValue string) (*StickyBucketAssignmentDoc, error)
SaveAssignments(doc *StickyBucketAssignmentDoc) error
GetAllAssignments(attributes map[string]string) (StickyBucketAssignments, error)
}

Key Features

  1. Version Control:

    • BucketVersion: Controls which version of the experiment a user is assigned to
    • MinBucketVersion: Blocks users from versions below this number
  2. Attribute-Based Bucketing:

    • HashAttribute: Primary attribute for bucketing (usually userId)
    • FallbackAttribute: Secondary attribute when primary is missing
  3. Thread Safety:

    • The in-memory implementation uses sync.RWMutex for concurrent access
    • Caching reduces database/service calls in high-traffic environments

Experiment Result Changes

The ExperimentResult returned by RunExperiment now includes a StickyBucketUsed boolean field that indicates if the variation was assigned from a sticky bucket:

result := client.RunExperiment(context.Background(), experiment)
if result.StickyBucketUsed {
// The user was assigned based on a previously stored assignment
}

Inline Experiments

Experiments can be defined and run using the Experiment type and the RunExperiment method of the client. Experiment definitions can be created directly as values of the Experiment type, or parsed from JSON using Go's json.Unmarshal function. Passing an Experiment value to the RunExperiment method will run the experiment and return an ExperimentResult that contains the resulting feature value. This approach allows users to run arbitrary experiments without providing feature definitions upfront.

experiment := &gb.Experiment{
Key: "my-experiment",
Variations: []gb.FeatureValue{"red", "blue", "green"},
}

result := client.RunExperiment(context.TODO(), experiment)

A full list of experiment fields can be found in the documentation .

When defining experiments, you can now use additional parameters for sticky bucketing:

experiment := &gb.Experiment{
Key: "my-experiment",
Variations: []gb.FeatureValue{"red", "blue", "green"},
// Sticky bucket parameters
BucketVersion: 1,
MinBucketVersion: 0,
HashAttribute: "userId",
FallbackAttribute: "deviceId",
}

Inline Experiment Return Value

A call to RunExperiment returns a value of type *ExperimentResult:

result := client.RunExperiment(context.TODO(), experiment)

// Whether the user is part of the experiment.
fmt.Println(result.InExperiment) // true or false

// The index of the assigned variation.
fmt.Println(result.VariationId) // 0 or 1

// The value of the assigned variation.
fmt.Println(result.Value) // "A" or "B"

// The user attribute used to assign a variation.
fmt.Println(result.HashAttribute) // "id"

// The value of that attribute.
fmt.Println(result.HashValue) // e.g., "123"

// Whether a sticky bucket assignment was used
fmt.Println(result.StickyBucketUsed) // true or false

The InExperiment flag is set to true only if the user was randomly assigned a variation. If the user fails any targeting rules or is forced into a specific variation, this flag will be false.

Experiment Tracking and Feature Usage Callbacks

The Go SDK provides comprehensive tracking capabilities for monitoring experiment exposures and feature evaluations.

Experiment Tracking Callback

The experiment callback is triggered when a user is included in an experiment. This is essential for analytics and experiment analysis.

import (
"context"
"log"
gb "github.com/growthbook/growthbook-golang"
)

// Define your user context
type UserContext struct {
UserID string
SessionID string
}

// Create experiment tracking callback
experimentCallback := func(ctx context.Context, exp *gb.Experiment, result *gb.ExperimentResult, extra any) {
userCtx, ok := extra.(UserContext)
if !ok {
log.Warn("Invalid extra data in experiment callback")
return
}

// Track experiment view in your analytics system
analytics.Track(analytics.Event{
UserID: userCtx.UserID,
Event: "Experiment Viewed",
Properties: map[string]interface{}{
"experiment_id": exp.Key,
"variation_id": result.VariationID,
"variation_value": result.Value,
"in_experiment": result.InExperiment,
"hash_used": result.HashUsed,
"session_id": userCtx.SessionID,
},
})

log.Printf("User %s entered experiment %s with variation %d",
userCtx.UserID, exp.Key, result.VariationID)
}

// Create client with tracking
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithSseDataSource(),
gb.WithExperimentCallback(experimentCallback),
)
if err != nil {
log.Fatal(err)
}
defer client.Close()
Tracking Timing

The experiment callback is only called when the user is actually included in the experiment. If they're excluded from the experiment due to targeting rules or sampling, this callback won't be triggered.

Feature Usage Callback

The feature usage callback is called every time a feature is evaluated, regardless of whether it's part of an experiment or not.

// Create feature usage callback
featureUsageCallback := func(ctx context.Context, featureKey string, result *gb.FeatureResult, extra any) {
userCtx, ok := extra.(UserContext)
if !ok {
log.Warn("Invalid extra data in feature usage callback")
return
}

// Track feature usage for monitoring and debugging
log.Printf("Feature %s evaluated for user %s: %v",
featureKey, userCtx.UserID, result.Value)

// Send to monitoring systems (e.g., DataDog, New Relic)
monitoring.RecordFeatureUsage(monitoring.FeatureUsageEvent{
FeatureKey: featureKey,
Value: result.Value,
Source: string(result.Source),
UserID: userCtx.UserID,
On: result.On,
})

// Track in analytics
analytics.Track(analytics.Event{
UserID: userCtx.UserID,
Event: "Feature Evaluated",
Properties: map[string]interface{}{
"feature_key": featureKey,
"feature_value": result.Value,
"source": result.Source,
"is_on": result.On,
},
})
}

// Create client with feature usage tracking
client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithSseDataSource(),
gb.WithFeatureUsageCallback(featureUsageCallback),
)

Combining Both Callbacks

You can use both callbacks together for comprehensive tracking:

type TrackingManager struct {
analytics Analytics
monitoring Monitoring
}

func (tm *TrackingManager) ExperimentCallback(
ctx context.Context,
exp *gb.Experiment,
result *gb.ExperimentResult,
extra any,
) {
userCtx := extra.(UserContext)

// Track in multiple systems
tm.analytics.Track(analytics.Event{
UserID: userCtx.UserID,
Event: "Experiment Viewed",
Properties: map[string]interface{}{
"experiment_id": exp.Key,
"variation_id": result.VariationID,
},
})

tm.monitoring.RecordExperiment(exp.Key, result.VariationID)
}

func (tm *TrackingManager) FeatureUsageCallback(
ctx context.Context,
featureKey string,
result *gb.FeatureResult,
extra any,
) {
userCtx := extra.(UserContext)

tm.monitoring.RecordFeatureUsage(featureKey, result.Value)

// Only track experiment-backed features in analytics
if result.Source == gb.ExperimentResultSource {
tm.analytics.Track(analytics.Event{
UserID: userCtx.UserID,
Event: "Feature Used (Experiment)",
Properties: map[string]interface{}{
"feature_key": featureKey,
"experiment_id": result.Experiment.Key,
},
})
}
}

// Initialize client with both callbacks
func NewGrowthBookClient(tm *TrackingManager) (*gb.Client, error) {
return gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithSseDataSource(),
gb.WithExperimentCallback(tm.ExperimentCallback),
gb.WithFeatureUsageCallback(tm.FeatureUsageCallback),
)
}

Using Extra Data

You can attach custom data that will be passed to each callback:

// Create a child client with user-specific extra data
userContext := UserContext{
UserID: "user-123",
SessionID: "session-abc",
}

childClient, err := client.WithAttributes(gb.Attributes{
"id": "user-123",
}).WithExtraData(userContext)

if err != nil {
log.Error("Failed to create child client:", err)
}

// Now when features are evaluated, the callbacks will receive userContext
result := childClient.EvalFeature(context.Background(), "new-checkout-flow")

Local vs Global Callbacks

Callbacks can be set globally on the main client or locally on child clients:

// Global callback (applies to all child clients)
globalClient, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithExperimentCallback(globalExperimentCallback),
)

// Local callback (overrides global for this child client)
childClient, err := globalClient.
WithAttributes(gb.Attributes{"id": "user-123"}).
WithExperimentCallback(specificExperimentCallback)

// This child uses the specific callback, not the global one
result := childClient.EvalFeature(context.Background(), "my-feature")

Integration Examples

Segment.io Integration

import "github.com/segmentio/analytics-go"

func createSegmentCallback(segmentClient analytics.Client) gb.ExperimentCallbackFunc {
return func(ctx context.Context, exp *gb.Experiment, result *gb.ExperimentResult, extra any) {
userID := extra.(string)

segmentClient.Enqueue(analytics.Track{
UserId: userID,
Event: "Experiment Viewed",
Properties: analytics.NewProperties().
Set("experimentId", exp.Key).
Set("variationId", result.VariationID).
Set("variationValue", result.Value),
})
}
}

client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithExperimentCallback(createSegmentCallback(segmentClient)),
)

DataDog Integration

import "github.com/DataDog/datadog-go/statsd"

func createDataDogFeatureCallback(statsd *statsd.Client) gb.FeatureUsageCallbackFunc {
return func(ctx context.Context, featureKey string, result *gb.FeatureResult, extra any) {
tags := []string{
fmt.Sprintf("feature:%s", featureKey),
fmt.Sprintf("source:%s", result.Source),
fmt.Sprintf("on:%t", result.On),
}

statsd.Incr("growthbook.feature.evaluated", tags, 1)

if result.On {
statsd.Incr("growthbook.feature.enabled", tags, 1)
}
}
}

client, err := gb.NewClient(
context.Background(),
gb.WithClientKey("sdk-abc123"),
gb.WithFeatureUsageCallback(createDataDogFeatureCallback(datadogClient)),
)

Logging

The SDK uses the slog logger instance. You can set up your own logger using the WithLogger option when calling the NewClient function. It is also possible to create a child GrowthBook client with its own logger, parameterized with additional data, as shown below:

func main() {
client, err := gb.NewClient(/* options */)
if err != nil {
log.Fatal(err)
}
// ...
}

func handleRequest(ctx context.Context, client *gb.Client, r request) {
traceId := ctx.Value("traceId")
logger := slog.Default().With("traceId", traceId)
client, err := client.WithLogger(logger)
if err != nil {
log.Fatal(err)
}

client, err = client.WithAttributes(gb.Attributes{"user_id": r.user_id})
if err != nil {
log.Fatal(err)
}

// Now, all calls to the client will use the local logger and attributes.
}

Further Reading


This version addresses grammatical issues and clarifies the text while maintaining the original content and code examples.

Supported Features

FeaturesAll versions

ExperimentationAll versions

Sticky Bucketing≥ v0.2.3

Prerequisites≥ v0.2.0

Saved Group References≥ v0.2.0

v2 Hashing≥ v0.1.4

Streaming≥ v0.1.4

SemVer Targeting≥ v0.1.4

Encrypted Features≥ v0.1.4