Feature flags в Go: как управлять релизами без страха и боли

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                

Блоги

Рекомендаций по книгам