Feature flags in Go: When Every Line of Code is a Risk
\n\nHave you ever woken up at 3 AM because a new release "killed" production? Or spent hours rolling back changes because a new feature broke the old logic? Sound familiar? Then feature flags are what will radically change your life as a Go developer.
\n\nFeature flags are not just "if-else" in code. They are a philosophy of safe releases. They are the ability to turn functionality on and off without deployment, conduct A/B testing, and perform canary releases. In the world of Go, where speed and reliability are top priorities, feature flags become a must-have tool.
\n\nWhat are Feature Flags and Why Do You Need Them in Go?
\n\nA feature flag (also known as a toggle) is a mechanism that allows you to enable or disable specific application functionality without changing code or redeploying. Simply put: you write code, wrap it in a condition, and if the flag is active, the code runs; if not, it is skipped.
\n\nIn Go, where compilation into a static binary and a minimalist architecture are the standard, feature flags solve several problems at once:
\n\n- \n
- Reduced release risk — rolled out a feature but something went wrong? Just turn off the flag. No rollback, no new deployment. \n
- A/B testing — show the new version to only 10% of users, compare metrics. \n
- Canary releases — gradually enable a feature for different user groups. \n
- Dark launches — code is already in production but hidden. Allows testing integrations without impacting users. \n
- Dependency management — enable a feature only when the backend infrastructure is ready. \n
How to Implement Feature Flags in Go: From Simple to Complex
\n\n1. The Simplest Way: Constants and Env Variables
\n\nLet's start with the basics. The most primitive but working option is to use global constants or environment variables. Suitable for an MVP or when you don't need dynamism.
\n\npackage main\n\nimport (\n "fmt"\n "os"\n)\n\nconst NewCheckoutFlowEnabled = false\n\nfunc main() {\n envFlag := os.Getenv("FEATURE_NEW_PAYMENT")\n isPaymentEnabled := envFlag == "true"\n\n if isPaymentEnabled {\n fmt.Println("New payment gateway is active")\n processNewPayment()\n } else {\n fmt.Println("Using old payment gateway")\n processOldPayment()\n }\n}\n\nfunc processNewPayment() {\n fmt.Println("Processing via Stripe API v3")\n}\n\nfunc processOldPayment() {\n fmt.Println("Processing via Stripe API v2")\n}\n\nThe drawbacks are obvious: no dynamic management, you need to restart the application. But for simple scenarios, it's okay.
\n\n2. Structural Approach: Your Own Flag Manager
\n\nWhen you have more than 5 flags, it's time to move the logic into a separate package. Let's create a simple feature flag manager with in-memory storage support.
\n\npackage feature\n\nimport (\n "sync"\n "time"\n)\n\ntype Flag struct {\n Name string\n Enabled bool\n UpdatedAt time.Time\n}\n\ntype Manager struct {\n mu sync.RWMutex\n flags map[string]Flag\n}\n\nfunc NewManager() *Manager {\n return &Manager{\n flags: make(map[string]Flag),\n }\n}\n\nfunc (m *Manager) IsEnabled(name string) bool {\n m.mu.RLock()\n defer m.mu.RUnlock()\n flag, exists := m.flags[name]\n return exists && flag.Enabled\n}\n\nfunc (m *Manager) Enable(name string) {\n m.mu.Lock()\n defer m.mu.Unlock()\n m.flags[name] = Flag{\n Name: name,\n Enabled: true,\n UpdatedAt: time.Now(),\n }\n}\n\nfunc (m *Manager) Disable(name string) {\n m.mu.Lock()\n defer m.mu.Unlock()\n m.flags[name] = Flag{\n Name: name,\n Enabled: false,\n UpdatedAt: