A type-safe, collision-free feature flag implementation for Go using context.
- Type-Safe: Uses Go generics for compile-time type safety
- Collision-Free: Each key is uniquely identified by its pointer address
- Context-Safe: Follows Go's context immutability guarantees
- Zero Dependencies: No external dependencies required
- Simple API: Easy to use with minimal boilerplate
go get github.com/mpyw/featureRequires Go 1.21 or later.
package main
import (
"context"
"fmt"
"github.com/mpyw/feature"
)
// Define a feature flag
var EnableNewUI = feature.NewNamedBool("new-ui")
func main() {
ctx := context.Background()
// Enable the feature
ctx = EnableNewUI.WithEnabled(ctx)
// Check if enabled
if EnableNewUI.Enabled(ctx) {
fmt.Println("New UI is enabled!")
}
}package main
import (
"context"
"fmt"
"github.com/mpyw/feature"
)
// Define a feature flag with a custom type
var MaxRetries = feature.New[int]()
func main() {
ctx := context.Background()
// Set a value
ctx = MaxRetries.WithValue(ctx, 5)
// Retrieve the value
retries := MaxRetries.Get(ctx)
fmt.Printf("Max retries: %d\n", retries)
}// Boolean feature flag
var MyFeature = feature.NewBool()
// Boolean feature flag with a debug name
var MyNamedFeature = feature.NewNamedBool("my-feature")
// Feature flag with custom type
var MyValueKey = feature.New[string]()
// Feature flag with custom type and debug name
var MyNamedValueKey = feature.NewNamed[string]("my-key")// Enable a feature
ctx = MyFeature.WithEnabled(ctx)
// Disable a feature
ctx = MyFeature.WithDisabled(ctx)
// Check if enabled
if MyFeature.Enabled(ctx) {
// Feature is enabled
}
// Get raw boolean value
value := MyFeature.Get(ctx)
// Check if the key is set in the context
value, exists := MyFeature.TryGet(ctx)// Set a value
ctx = MyValueKey.WithValue(ctx, "hello")
// Get the value (returns zero value if not set)
value := MyValueKey.Get(ctx)
// Try to get the value (returns value and bool indicating if set)
value, exists := MyValueKey.TryGet(ctx)
if exists {
fmt.Printf("Value: %s\n", value)
}When using context.WithValue directly with string or int keys, collisions are easy:
// β BAD: These keys will collide!
type key string
var userKey key = "user"
var requestKey key = "user" // Same underlying value
ctx = context.WithValue(ctx, userKey, "Alice")
ctx = context.WithValue(ctx, requestKey, "Bob")
// userKey now returns "Bob" instead of "Alice"!You might think: "Just use type contextKey struct{} for each key!"
// π€ Avoids collisions, but has other problems
type userIDKey struct{}
type requestIDKey struct{}
var userID = userIDKey{}
var requestID = requestIDKey{}
ctx = context.WithValue(ctx, userID, 123)
ctx = context.WithValue(ctx, requestID, "abc")
// β Type assertions required, can panic
userIDValue := ctx.Value(userID).(int)
requestIDValue := ctx.Value(requestID).(string)Problems:
- Need a unique type for each key (boilerplate)
- No compile-time type safety for values
- Requires type assertions everywhere (runtime panics possible)
You might think: "What if a library wrapped those keys and handled assertions?"
// π€ Still need to define key types
type userIDKey struct{}
type requestIDKey struct{}
// Library wraps keys with generic type
var UserID = feature.Wrap[int](userIDKey{})
var RequestID = feature.Wrap[string](requestIDKey{})
ctx = UserID.Set(ctx, 123)
value := UserID.Get(ctx) // Library handles assertionProblems:
- Still need to define a unique type for each key (boilerplate)
- Key type and value type are separate - nothing prevents:
// In file A var UserID = feature.Wrap[int](userIDKey{}) // In file B (accidentally) var UserName = feature.Wrap[string](userIDKey{}) // Same key type, different value type!
- No compile-time guarantee that key type β value type mapping is consistent
- Two places to define things: the type definition and the var declaration
// β
BEST: Collision-free, type-safe, ergonomic
var UserID = feature.New[int]()
var RequestID = feature.New[string]()
ctx = UserID.WithValue(ctx, 123)
ctx = RequestID.WithValue(ctx, "abc")
// β
No type assertions, compile-time safety
userIDValue := UserID.Get(ctx) // int
requestIDValue := RequestID.Get(ctx) // string
// β
Rich API
if UserID.IsSet(ctx) {
fmt.Println("User ID is set")
}
value, ok := RequestID.TryGet(ctx)
config := SomeKey.GetOrDefault(ctx, defaultValue)
required := RequiredKey.MustGet(ctx) // Panics with clear message if not setBenefits:
- Pointer identity: Each
varholds a unique pointer, preventing collisions - Type safety: Generics ensure compile-time type checking
- No allocations: Keys are allocated once as package-level variables
- Rich API: Get, TryGet, GetOrDefault, MustGet, IsSet, IsNotSet, DebugValue
- Better debugging: Named keys show up clearly in logs and error messages
- Boolean keys: Special
BoolKeytype with Enabled/Disabled/ExplicitlyDisabled methods - Three-state logic: Distinguish between unset, explicitly true, and explicitly false
A similar idea was proposed to the Go team in 2021:
The proposal by @dsnet (Joe Tsai, a Go team member) suggests:
type Key[Value any] struct { name *string }
func NewKey[Value any](name string) Key[Value] {
return Key[Value]{&name} // Uses argument address for uniqueness
}
func (k Key[V]) WithValue(ctx Context, val V) Context
func (k Key[V]) Value(ctx Context) (V, bool)This package implements essentially the same concept. However, the official proposal has been on hold for over 3 years, primarily because:
- Standard library generics policy is undecided - Discussion #48287 is still ongoing about how to add generics to existing packages
- Migration path unclear - Whether to deprecate
context.WithValue/context.Valueor keep both APIs - Alternative proposals being considered - Multiple approaches are being evaluated in parallel
This package provides an immediate, production-ready solution while the Go team deliberates.
Unlike the proposal's struct-based approach, this package uses the Sealed Interface pattern:
type Key[V any] interface {
WithValue(ctx context.Context, value V) context.Context
Get(ctx context.Context) V
TryGet(ctx context.Context) (V, bool)
// ... other methods
downcast() key[V] // unexported method prevents external implementation
}
type key[V any] struct { // unexported implementation
name string
ident *opaque
}Why this matters:
The struct-based approach has a subtle vulnerability. In Go, you can bypass constructor functions and directly initialize structs with zero values for unexported fields:
// With struct-based design:
type Key[V any] struct { name *string }
// This compiles! Both keys have nil name pointer
badKeyX := Key[int]{}
badKeyY := Key[string]{}
// These will COLLIDE because both use (*string)(nil) as identityNote: (*T)(nil) doesn't panic like nil does - it silently uses the zero value as the key, making collisions hard to detect.
With the Sealed Interface pattern:
- The implementation struct
key[V]is unexported, preventing direct initialization - The interface contains an unexported method
downcast(), preventing external implementations - Users must use
feature.New()orfeature.NewBool()to create keys
Additional benefit: BoolKey can be used anywhere Key[bool] is expected, providing better interoperability than struct embedding would allow.
The internal opaque type that provides pointer identity includes a byte field:
type opaque struct {
_ byte // Prevents address optimization
}Without this, Go's compiler optimization would give all zero-size struct pointers the same address:
type empty struct{}
a := new(empty)
b := new(empty)
fmt.Printf("%p %p\n", a, b) // Same address! Keys would collide.package myapp
import "github.com/mpyw/feature"
// Define keys at package level to ensure single instance
var (
EnableBetaFeature = feature.NewNamedBool("beta-feature")
MaxConcurrency = feature.New[int]()
)// Named keys make debugging easier
var MyFeature = feature.NewNamedBool("my-feature")
fmt.Println(MyFeature)
// Output: my-feature
// Anonymous keys automatically include call site information for debugging
var AnonFeature = feature.NewBool()
fmt.Println(AnonFeature)
// Output: anonymous(/path/to/file.go:42)@0x14000010098// Instead of interface{}, use specific types
var MaxRetries = feature.New[int]()
var UserID = feature.New[string]()
var Config = feature.New[*AppConfig]()Each key holds an internal *opaque pointer that serves as its unique identity. This ensures:
- Each key has a unique identity based on the internal pointer
- Keys can be used as context keys without collisions
- Type safety is maintained through generics
- Even if the key struct is copied, the identity remains the same (copy-safe)
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- context - Go's official context package
- proposal: context: add generic key and value type #49189 - Go official proposal