Feature flags в Go: когда каждая строчка кода — это риск
Вы когда-нибудь просыпались в 3 часа ночи от того, что новый релиз «убил» продакшен? Или тратили часы на откат изменений, потому что новая фича сломала логику старой? Знакомо? Тогда feature flags (флаги функций) — это то, что кардинально изменит вашу жизнь как Go-разработчика.
Feature flags — это не просто «if-else» в коде. Это философия безопасных релизов. Это возможность включать и выключать функциональность без деплоя, проводить A/B тестирование и делать canary-релизы. В мире Go, где скорость и надежность — главные приоритеты, feature flags становятся must-have инструментом.
Что такое feature flags и зачем они нужны в Go?
Feature flag (он же toggle, фича-флаг) — это механизм, который позволяет включать или отключать определенный функционал приложения без изменения кода и повторного деплоя. Простыми словами: вы пишете код, оборачиваете его в условие, и если флаг активен — код выполняется, если нет — пропускается.
В Go, где компиляция в статический бинарник и минималистичная архитектура — стандарт, feature flags решают сразу несколько проблем:
- Снижение риска релизов — выкатили фичу, но что-то пошло не так? Просто выключили флаг. Без отката, без нового деплоя.
- A/B тестирование — показываем новую версию только 10% пользователей, сравниваем метрики.
- Canary releases — постепенное включение фичи для разных групп пользователей.
- Dark launches — код уже в продакшене, но скрыт. Позволяет тестировать интеграции без влияния на пользователей.
- Управление зависимостями — включаем фичу только когда готова бэкенд-инфраструктура.
Как реализовать feature flags в Go: от простого к сложному
1. Самый простой способ: константы и env-переменные
Начнем с базы. Самый примитивный, но рабочий вариант — использовать глобальные константы или переменные окружения. Подходит для MVP или когда не нужна динамика.
package main
import (
"fmt"
"os"
)
const NewCheckoutFlowEnabled = false
func main() {
envFlag := os.Getenv("FEATURE_NEW_PAYMENT")
isPaymentEnabled := envFlag == "true"
if isPaymentEnabled {
fmt.Println("Новый платежный шлюз активен")
processNewPayment()
} else {
fmt.Println("Используется старый платежный шлюз")
processOldPayment()
}
}
func processNewPayment() {
fmt.Println("Обработка через Stripe API v3")
}
func processOldPayment() {
fmt.Println("Обработка через Stripe API v2")
}
Минусы очевидны: нет динамического управления, нужно перезапускать приложение. Но для простых сценариев — ок.
2. Структурный подход: свой менеджер флагов
Когда флагов становится больше 5, пора выносить логику в отдельный пакет. Создадим простой менеджер feature flags с поддержкой in-memory хранения.
package feature
import (
"sync"
"time"
)
type Flag struct {
Name string
Enabled bool
UpdatedAt time.Time
}
type Manager struct {
mu sync.RWMutex
flags map[string]Flag
}
func NewManager() *Manager {
return &Manager{
flags: make(map[string]Flag),
}
}
func (m *Manager) IsEnabled(name string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
flag, exists := m.flags[name]
return exists && flag.Enabled
}
func (m *Manager) Enable(name string) {
m.mu.Lock()
defer m.mu.Unlock()
m.flags[name] = Flag{
Name: name,
Enabled: true,
UpdatedAt: time.Now(),
}
}
func (m *Manager) Disable(name string) {
m.mu.Lock()
defer m.mu.Unlock()
m.flags[name] = Flag{
Name: name,
Enabled: false,
UpdatedAt: time.Now(),
}
}
Теперь используем в коде:
package main
import (
"fmt"
"yourproject/feature"
)
func main() {
fm := feature.NewManager()
fm.Enable("new-dashboard")
if fm.IsEnabled("new-dashboard") {
fmt.Println("Показываем новый дашборд")
renderNewDashboard()
} else {
fmt.Println("Показываем старый дашборд")
renderOldDashboard()
}
}
Уже лучше, но флаги сбрасываются при перезапуске. Нужно сохранять состояние.
3. Feature flags с Redis: динамическое управление
Добавим Redis как backend для хранения флагов. Это даст возможность менять флаги на лету через админку или API.
package feature
import (
"context"
"time"
"github.com/go-redis/redis/v8"
)
type RedisManager struct {
client *redis.Client
ctx context.Context
}
func NewRedisManager(addr string) *RedisManager {
rdb := redis.NewClient(&redis.Options{
Addr: addr,
})
return &RedisManager{
client: rdb,
ctx: context.Background(),
}
}
func (r *RedisManager) IsEnabled(name string) bool {
val, err := r.client.Get(r.ctx, "feature:"+name).Result()
if err != nil {
return false
}
return val == "1"
}
func (r *RedisManager) Set(name string, enabled bool) error {
val := "0"
if enabled {
val = "1"
}
return r.client.Set(r.ctx, "feature:"+name, val, 0).Err()
}
// TTL-версия флага (автоотключение через 24 часа)
func (r *RedisManager) SetWithTTL(name string, enabled bool, ttl time.Duration) error {
val := "0"
if enabled {
val = "1"
}
return r.client.Set(r.ctx, "feature:"+name, val, ttl).Err()
}
Теперь вы можете управлять флагами через отдельный HTTP-эндпоинт или админ-панель. Это уже production-ready решение.
Продвинутые техники: A/B тестирование и canary releases
A/B тестирование с feature flags в Go
Разделим пользователей на группы по ID. Например, 20% получают новую фичу, 80% — старую.
package abtest
import (
"hash/fnv"
)
type Experiment struct {
Name string
Percentage int // 0-100
}
func (e *Experiment) IsInTreatment(userID string) bool {
hash := fnv.New32a()
hash.Write([]byte(userID + ":" + e.Name))
bucket := int(hash.Sum32() % 100)
return bucket < e.Percentage
}
// Использование
func main() {
exp := &Experiment{Name: "new-checkout", Percentage: 20}
userID := "user_12345"
if exp.IsInTreatment(userID) {
showNewCheckout()
} else {
showOldCheckout()
}
}
Canary release: постепенное включение
Можно комбинировать feature flags с мониторингом ошибок. Если error rate растет — автоматически отключаем фичу.
type CanaryRelease struct {
FeatureName string
Threshold float64 // максимальный error rate (0