A flexible, performant, and production-ready feature flag and A/B testing SDK for Go.
- ✨ Simple on/off feature flags - Control features with boolean flags
- 📊 Percentage-based rollouts - Gradually roll out features with deterministic hashing
- 🎯 Conditional targeting - Target users based on attributes (country, plan, custom fields)
- 🧪 A/B testing - Run experiments with multiple variants
- ⏱️ Switchback testing - Time-based experimentation for marketplace and system-wide tests
- 🔒 Thread-safe - Safe for concurrent access
- 📝 JSON/YAML configuration - Load flags from configuration files
- 🎨 Flexible operators - Support for ==, !=, in, >, <, contains, regex, and more
- 🚀 Zero dependencies (except yaml parser)
- 📦 Clean API - Simple and intuitive interface
go get github.com/pedrampdd/toggopackage main
import (
"fmt"
"github.com/pedrampdd/toggo"
)
func main() {
// Create a feature flag store
store := toggo.NewStore()
// Define a feature flag
flag := &toggo.Flag{
Name: "new_checkout",
Enabled: true,
Rollout: 50, // 50% of users
}
store.AddFlag(flag)
// Check if enabled for a user
ctx := toggo.Context{
"user_id": "12345",
"country": "US",
}
if store.IsEnabled("new_checkout", ctx) {
// Show new checkout flow
}
}A Context is a map of user attributes used for flag evaluation:
ctx := toggo.Context{
"user_id": "12345",
"country": "US",
"plan": "premium",
"age": 25,
}Flags control feature availability:
flag := &toggo.Flag{
Name: "feature_name",
Enabled: true,
Rollout: 100, // 0-100 percentage
RolloutKey: "user_id", // Context key for hashing (default: "user_id")
Conditions: []toggo.Condition{
// Optional targeting conditions
},
}Target specific users with conditions:
condition := toggo.Condition{
Attribute: "country",
Operator: toggo.OperatorIn,
Value: []interface{}{"US", "CA", "UK"},
}| Operator | Description | Example |
|---|---|---|
== |
Equal | plan == "premium" |
!= |
Not equal | country != "US" |
in |
In list | country in ["US", "CA"] |
not_in |
Not in list | country not_in ["DE", "FR"] |
> |
Greater than | age > 18 |
>= |
Greater than or equal | age >= 21 |
< |
Less than | age < 65 |
<= |
Less than or equal | age <= 25 |
contains |
String contains | email contains "@company.com" |
starts_with |
String starts with | name starts_with "John" |
ends_with |
String ends with | file ends_with ".pdf" |
regex |
Regex match | email regex ".*@example\\.com" |
store := toggo.NewStore()
flag := &toggo.Flag{
Name: "dark_mode",
Enabled: true,
Rollout: 100,
}
store.AddFlag(flag)
ctx := toggo.Context{"user_id": "123"}
if store.IsEnabled("dark_mode", ctx) {
// Enable dark mode
}flag := &toggo.Flag{
Name: "new_ui",
Enabled: true,
Rollout: 25, // 25% of users
RolloutKey: "user_id",
}
store.AddFlag(flag)
// Same user always gets same result (deterministic)
ctx := toggo.Context{"user_id": "user_42"}
enabled := store.IsEnabled("new_ui", ctx) // Consistent for this userflag := &toggo.Flag{
Name: "premium_feature",
Enabled: true,
Rollout: 100,
Conditions: []toggo.Condition{
{
Attribute: "plan",
Operator: toggo.OperatorEqual,
Value: "premium",
},
{
Attribute: "country",
Operator: toggo.OperatorIn,
Value: []interface{}{"US", "CA", "UK"},
},
},
}
store.AddFlag(flag)
ctx := toggo.Context{
"user_id": "123",
"plan": "premium",
"country": "US",
}
// Enabled only if ALL conditions match
if store.IsEnabled("premium_feature", ctx) {
// Show premium feature
}flag := &toggo.Flag{
Name: "pricing_test",
Enabled: true,
RolloutKey: "user_id",
DefaultVariant: "control",
Variants: []toggo.Variant{
{Name: "control", Weight: 33},
{Name: "price_low", Weight: 33},
{Name: "price_high", Weight: 34},
},
}
store.AddFlag(flag)
ctx := toggo.Context{"user_id": "user_42"}
variant, _ := store.GetVariant("pricing_test", ctx)
switch variant {
case "control":
price = 99.99
case "price_low":
price = 79.99
case "price_high":
price = 119.99
}Switchback testing is a time-based experimentation method where all users see the same variant at the same time, and the variant switches at regular intervals. This is useful for:
- Testing marketplace interventions (e.g., driver incentives, pricing strategies)
- Comparing system-wide behaviors that can't be tested per-user
- Controlling for time-of-day effects by alternating patterns daily
// Create a store with switchback strategy
store := toggo.NewStore(
toggo.WithSwitchback(
toggo.WithIntervalMinutes(30), // Switch every 30 minutes
toggo.WithDailySwap(true), // Reverse pattern each day
),
)
// Define variants to switch between
flag := &toggo.Flag{
Name: "driver_rebate",
Enabled: true,
DefaultVariant: "standard_rebate",
Variants: []toggo.Variant{
{Name: "standard_rebate", Weight: 50}, // 10% cashback
{Name: "premium_rebate", Weight: 50}, // 15% cashback
},
}
store.AddFlag(flag)
// All drivers get the same rebate type at the same time
ctx := toggo.Context{"driver_id": "DRV-12345"}
rebateType, _ := store.GetVariant("driver_rebate", ctx)
switch rebateType {
case "standard_rebate":
applyCashback(0.10)
case "premium_rebate":
applyCashback(0.15)
}
// Check timing information
if info := toggo.GetSwitchbackInfo(store); info != nil {
fmt.Printf("Current interval: %d\n", info.CurrentInterval)
fmt.Printf("Time until next switch: %v\n", info.TimeUntilSwitch)
}Switchback Schedule Example (30-minute intervals, 2 variants):
Day 0:
- 00:00-00:30 → standard_rebate
- 00:30-01:00 → premium_rebate
- 01:00-01:30 → standard_rebate
- ... (pattern continues)
Day 1 (with daily swap):
- 00:00-00:30 → premium_rebate (reversed)
- 00:30-01:00 → standard_rebate (reversed)
- 01:00-01:30 → premium_rebate (reversed)
- ... (pattern continues)
Key Differences from Standard A/B Testing:
- Standard A/B: Each user is randomly assigned to a variant (stays consistent)
- Switchback: All users see the same variant at the same time (switches periodically)
{
"flags": [
{
"name": "new_checkout",
"enabled": true,
"rollout": 50,
"conditions": [
{
"attribute": "country",
"operator": "in",
"value": ["US", "CA"]
}
]
}
]
}import "github.com/pedrampdd/toggo/loader"
store := toggo.NewStore()
l := loader.NewJSONFile("flags.json")
l.LoadIntoStore(store)flags:
- name: dark_mode
enabled: true
rollout: 100
- name: beta_features
enabled: true
rollout: 25
conditions:
- attribute: beta_tester
operator: "=="
value: trueimport "github.com/pedrampdd/toggo/loader"
store := toggo.NewStore()
l := loader.NewYAMLFile("flags.yaml")
l.LoadIntoStore(store)Creates a new feature flag store.
Adds or updates a flag in the store. Returns error if validation fails.
Checks if a feature flag is enabled for the given context. Returns false if flag not found or conditions don't match.
Returns the variant name for A/B testing. Second return value indicates if flag is enabled.
Retrieves a flag by name. Returns ErrFlagNotFound if not found.
Returns all flag names.
Removes a flag from the store.
Removes all flags from the store.
Returns the number of flags in the store.
type Flag struct {
Name string
Enabled bool
Rollout int // 0-100
RolloutKey string // Default: "user_id"
Conditions []Condition
Variants []Variant
DefaultVariant string
}type Condition struct {
Attribute string
Operator Operator
Value interface{}
Negate bool
}type Variant struct {
Name string
Weight int // 0-100
Conditions []Condition
}toggo/
├── toggo.go # Main package file with documentation
├── context.go # Context type and methods
├── flag.go # Flag and Variant types
├── condition.go # Condition type
├── operator.go # Operator constants
├── store.go # Store implementation
├── rollout.go # Rollout strategy
├── errors.go # Error definitions
├── internal/ # Internal implementation details
│ ├── evaluator/ # Condition evaluation logic
│ └── hash/ # Hashing for rollouts
├── loader/ # Configuration loaders
│ ├── json.go
│ └── yaml.go
├── examples/ # Usage examples
│ ├── simple/
│ ├── abtest/
│ ├── conditional/
│ └── config_loader/
└── testdata/ # Test fixtures
Run all tests:
go test ./...Run with coverage:
go test -cover ./...Run specific package:
go test ./internal/evaluatorExplore the examples/ directory for complete working examples:
- simple - Basic feature flag usage
- abtest - A/B testing with variants
- conditional - Conditional targeting
- switchback - Time-based switchback experiments
- config_loader - Loading flags from JSON/YAML
Run an example:
cd examples/simple
go run main.go-
Use deterministic rollout keys - Always use stable user identifiers (user_id, session_id) for rollout keys to ensure consistent experience.
-
Validate flags - Flags are validated when added to the store. Handle errors appropriately.
-
Keep conditions simple - Complex condition trees can impact performance. Consider splitting into multiple flags.
-
Use variants for A/B tests - Don't use multiple flags for variants of the same experiment.
-
Load from config files - Store flag definitions in version-controlled YAML/JSON files for easier management.
-
Test coverage - Always test both enabled and disabled states of features.
- Thread-safe - Uses
sync.RWMutexfor concurrent reads - Fast evaluation - O(1) flag lookup, O(n) condition evaluation where n is number of conditions
- Deterministic hashing - FNV-1a hash for consistent, fast rollout decisions
- Zero allocations - Designed to minimize allocations in hot paths
- Remote flag management integration
- Metrics and analytics hooks
- Flag scheduling (enable/disable at specific times)
- User segments for reusable targeting
- Admin UI for flag management
- WebSocket/SSE for real-time flag updates
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details
Built with ❤️ for the Go community
Questions? Open an issue or start a discussion!