Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
228: Feature Flags in Go Services
A few years ago, I was on a team launching a new pricing engine for a B2B SaaS product. We spent three months building it, tested it in staging until we were blue in the face, and finally merged it into the main branch. The moment we deployed to production, we realized we'd missed a critical edge case for customers with grandfathered legacy contracts. The site started throwing 500 errors for our biggest spenders. We had to scramble to revert the commit, wait for the CI/CD pipeline to build and deploy the old version, and pray that no data had been corrupted in those ten minutes of chaos. It was a stressful morning, and it taught me a lesson I'll never forget: deploying code should not be the same thing as releasing a feature.
Feature flags—or toggles—allow you to decouple those two events. You deploy the code to production, but it stays dormant. You then "flip a switch" (via a config file, a database entry, or a third-party service) to enable the feature for specific users or a small percentage of traffic. If things go south, you flip the switch back. No reverts, no emergency pipelines, and no panicked Slack messages from the CEO.
Decoupling Logic with the Provider Pattern
In Go, you don't want to scatter if flagEnabled { ... } blocks all over your business logic. That makes your code unreadable and incredibly hard to test. Instead, I recommend treating your feature flags as a dependency. By defining a FeatureProvider interface, you can swap out a hard-coded local implementation for a dynamic one (like LaunchDarkly or an internal API) without changing your core service logic.
type FeatureProvider interface {
IsEnabled(featureKey string, userID string) bool
}
type PaymentService struct {
flags FeatureProvider
legacy PaymentProcessor
modern PaymentProcessor
}
func (s *PaymentService) Process(userID string, amount float64) error {
// Instead of a hardcoded boolean, we ask the provider
if s.flags.IsEnabled("use-modern-gateway", userID) {
return s.modern.Charge(amount)
}
return s.legacy.Charge(amount)
}
Notice how the PaymentService doesn't actually know how the flag is determined. It just knows that for a given user, the feature is either on or off. This makes your unit tests trivial: you can just pass a mock provider that returns true for one test and false for another.
Handling Dynamic State and Concurrency
When you start moving flags out of static config files and into a database or an external API, you run into a classic Go problem: concurrency. You can't just read a map from a remote server on every single request; you'll kill your performance and likely hammer your API limits. I usually implement a local cache with a background goroutine that refreshes the flag states every few minutes.
You'll want to use a sync.RWMutex to ensure that your service can read flags concurrently while the background updater is writing to the cache. If you're building something at massive scale, you might look into atomic.Value to store the entire configuration map, which allows for lock-free reads.
type LocalFlagProvider struct {
mu sync.RWMutex
flags map[string]bool
}
func (p *LocalFlagProvider) IsEnabled(key string, _ string) bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.flags[key]
}
func (p *LocalFlagProvider) updateFlags(newFlags map[string]bool) {
p.mu.Lock()
defer p.mu.Unlock()
p.flags = newFlags
}
One warning: feature flags are technical debt by design. If you leave a flag in the code after a feature is 100% rolled out, you're just adding cognitive load for the next developer. I make it a rule to create a "cleanup ticket" in the backlog the moment the feature flag is created. If you don't, your codebase will eventually look like a jungle of dead if/else blocks.
📋 Practical Task
Exercise: Implementing a Canary Toggle for a User Profile Migration
You are migrating a user profile system from a legacy SQL table to a new Document store. You cannot switch everyone at once; you need to implement a "Canary" release where only users with an even-numbered UserID see the new profile system.
Requirements:
- Create a
FeatureProviderinterface with anIsEnabled(featureKey, userID string) boolmethod. - Implement a
CanaryProviderstruct that satisfies this interface. TheIsEnabledmethod should returntrueonly if thefeatureKeyis "use-new-profile-store" AND theuserID(which is a numeric string) represents an even number. - Create a
ProfileServicethat takes theFeatureProvideras a dependency. It should have a methodGetProfile(userID string)that returns "Legacy Profile" or "Modern Profile" based on the flag. - Write a
mainfunction that tests this with at least two different UserIDs (one even, one odd) to verify the routing logic.
There are no comments for now.