← Voltar para o blog

Feature Flags em Go: Rollout Seguro sem Medo

Aprenda feature flags em Go para rollout gradual, canary release, rollback rápido, testes, observabilidade e deploy seguro em APIs e workers.

Feature flags em Go são uma forma prática de separar deploy de lançamento. O código novo pode ir para produção desligado, ser ativado primeiro para um grupo pequeno, ganhar tráfego aos poucos e ser desligado rapidamente se métricas ou logs mostrarem problema. Para times que trabalham com APIs, workers, sistemas financeiros, marketplaces, SaaS ou plataformas internas, essa separação reduz o medo de deploy e melhora a velocidade de entrega.

Go combina bem com esse padrão porque favorece configuração explícita, binários simples, testes rápidos e serviços pequenos. Uma flag não precisa virar uma plataforma complexa no primeiro dia. Em muitos projetos, um mapa de configuração carregado no startup, uma tabela no banco ou um client para um serviço de feature management já resolvem. O ponto importante é tratar a flag como parte do desenho operacional, não como um if jogado no meio do handler.

Este guia mostra quando usar feature flags, como modelar flags em Go, como testar caminhos novos e antigos, como observar rollout em produção e quais erros evitar. Ele complementa os guias de graceful shutdown em Go, Docker para Go, OpenTelemetry em Go e Kubernetes com Go.

O que uma feature flag resolve

Sem feature flag, deploy e lançamento acontecem no mesmo instante. Assim que o binário novo entra no ar, todo usuário passa pelo código novo. Se a mudança quebra uma rota, aumenta latência, muda contrato de API ou aciona um comportamento inesperado em worker, o rollback precisa devolver o deploy inteiro. Isso pode demorar, depender de pipeline, reverter migrations ou afetar mudanças que estavam saudáveis.

Com feature flag, você pode fazer outro fluxo:

  1. Enviar o código novo para produção com a flag desligada.
  2. Validar que o binário inicializa, conecta nas dependências e passa nos health checks.
  3. Ativar a flag para ambiente interno ou uma conta de teste.
  4. Aumentar o público gradualmente.
  5. Monitorar erro, latência, volume, fila e métricas de negócio.
  6. Desligar a flag se algo piorar, sem novo deploy.

Esse padrão é especialmente útil para mudanças que alteram comportamento sem alterar toda a arquitetura: novo algoritmo de cálculo, nova integração externa, nova regra de autorização, novo fluxo de checkout, novo consumidor de fila, nova estratégia de cache ou troca gradual entre duas implementações.

Tipos de flags em sistemas Go

Nem toda flag tem o mesmo propósito. Misturar tudo com um único nome genérico, como enable_new_flow, dificulta revisão e limpeza. Uma classificação simples ajuda.

Release flags controlam lançamento de funcionalidades. Exemplo: ativar uma nova tela ou endpoint para 5% dos usuários.

Operational flags mudam comportamento operacional. Exemplo: reduzir concorrência de um worker, desligar temporariamente uma integração lenta ou trocar uma estratégia de retry.

Permission flags liberam recurso para clientes, planos ou grupos específicos. Exemplo: habilitar exportação avançada apenas para contas premium.

Experiment flags sustentam testes A/B. Elas precisam de segmentação estável e métricas bem definidas; caso contrário, viram aleatoriedade sem aprendizado.

Em Go, essa distinção costuma aparecer no pacote que consulta flags e no nome das constantes. Uma flag de release pode ter vida curta. Uma flag de permissão talvez vire regra permanente de produto. Uma flag operacional precisa de documentação de emergência e dono claro.

Um modelo mínimo e explícito

Comece com uma interface pequena. Evite acoplar todo o código a um SDK específico. O handler, service ou worker só precisa perguntar se uma flag está ativa para um contexto.

package feature

import "context"

type Context struct {
    UserID    string
    AccountID string
    Email     string
    Plan      string
    Region    string
}

type Flags interface {
    Enabled(ctx context.Context, name string, fc Context) bool
}

No começo, uma implementação em memória pode bastar:

type StaticFlags struct {
    values map[string]bool
}

func NewStaticFlags(values map[string]bool) StaticFlags {
    return StaticFlags{values: values}
}

func (s StaticFlags) Enabled(ctx context.Context, name string, fc Context) bool {
    return s.values[name]
}

Esse desenho simples já melhora testabilidade. Em produção, você pode trocar StaticFlags por uma implementação que lê de banco, Redis, arquivo de configuração ou serviço externo. O resto da aplicação continua recebendo a interface.

Use constantes, não strings soltas

Strings soltas espalhadas pelo código são difíceis de renomear e fáceis de digitar errado. Prefira declarar flags conhecidas em um pacote pequeno:

package feature

const (
    CheckoutPixV2       = "checkout_pix_v2"
    AsyncInvoiceWorker  = "async_invoice_worker"
    ShippingQuoteV2     = "shipping_quote_v2"
    SearchRankingV3     = "search_ranking_v3"
)

Isso não impede configuração dinâmica. Apenas torna o código revisável. Quando alguém busca CheckoutPixV2, encontra todos os pontos que dependem da flag. Quando a flag expira, fica claro o que remover.

Também vale manter metadados fora do binário: descrição, dono, data de criação, data esperada de remoção, risco e link para plano de rollout. Em times pequenos, isso pode ser um arquivo YAML versionado. Em times maiores, costuma ser responsabilidade da plataforma de feature flags.

Exemplo em um handler HTTP

Imagine uma API que está migrando cálculo de frete para uma implementação nova. O handler não deve saber detalhes de rollout, mas pode escolher o serviço com base na flag.

func (h Handler) QuoteShipping(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    accountID := r.Header.Get("X-Account-ID")

    fc := feature.Context{AccountID: accountID}

    var quote Quote
    var err error

    if h.flags.Enabled(ctx, feature.ShippingQuoteV2, fc) {
        quote, err = h.shippingV2.Quote(ctx, accountID)
    } else {
        quote, err = h.shippingV1.Quote(ctx, accountID)
    }
    if err != nil {
        http.Error(w, "erro ao calcular frete", http.StatusBadGateway)
        return
    }

    _ = json.NewEncoder(w).Encode(quote)
}

Em código real, escolha nomes coerentes com o domínio. O ponto é manter o if perto da decisão de comportamento, sem duplicar autenticação, parsing, validação e resposta HTTP. Se a ramificação nova exige muitas mudanças, extraia uma interface de serviço e deixe o handler fino.

Segmentação estável

Rollout percentual precisa ser estável. Se cada requisição sorteia true ou false, o mesmo usuário pode ver dois comportamentos em minutos diferentes. Isso confunde cache, suporte, métricas e experiência.

Uma abordagem comum é hashear uma chave estável, como accountID, e mapear para 0-99:

func bucket(key string) int {
    h := fnv.New32a()
    _, _ = h.Write([]byte(key))
    return int(h.Sum32() % 100)
}

func enabledForPercent(accountID string, percent int) bool {
    if percent <= 0 {
        return false
    }
    if percent >= 100 {
        return true
    }
    return bucket(accountID) < percent
}

Com isso, uma conta que caiu no bucket 17 continuará no bucket 17. Quando o rollout sobe de 10% para 25%, ela entra. Quando cai para 5%, sai. Essa previsibilidade ajuda suporte e investigação.

Escolha a chave com cuidado. Para uma regra de cobrança, accountID costuma ser melhor que userID, porque todos os usuários da mesma conta precisam do mesmo comportamento. Para um experimento de interface, userID pode fazer mais sentido.

Teste os dois caminhos

Feature flag não reduz necessidade de teste; ela aumenta a obrigação de testar os dois caminhos enquanto a flag existir. Table-driven tests funcionam muito bem para isso.

func TestQuoteShipping(t *testing.T) {
    tests := []struct {
        name    string
        enabled bool
        wantSvc string
    }{
        {name: "fluxo antigo", enabled: false, wantSvc: "v1"},
        {name: "fluxo novo", enabled: true, wantSvc: "v2"},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            flags := feature.NewStaticFlags(map[string]bool{
                feature.ShippingQuoteV2: tt.enabled,
            })
            got := chooseShippingService(flags)
            if got != tt.wantSvc {
                t.Fatalf("serviço = %s; esperado %s", got, tt.wantSvc)
            }
        })
    }
}

Combine isso com table-driven tests em Go, testes em Go e pipeline de TDD e CI/CD. Se o caminho novo mexe com banco, inclua teste de integração. Se mexe com fila, valide idempotência, retry e DLQ. Se mexe com autorização, teste casos negados, não apenas o caminho feliz.

Observabilidade por variação

Uma flag em produção precisa aparecer em logs, métricas e traces. Sem isso, você não consegue responder se o erro aumentou no grupo novo ou no tráfego geral.

Em logs estruturados com slog, inclua a flag e a variação nos eventos relevantes:

slog.InfoContext(ctx, "cotação calculada",
    "feature", feature.ShippingQuoteV2,
    "feature_enabled", enabled,
    "account_id", accountID,
    "duration_ms", duration.Milliseconds(),
)

Em métricas, cuidado com cardinalidade. Não coloque userID como label. Labels como feature="checkout_pix_v2" e variation="on" podem ser aceitáveis se o número de flags monitoradas for pequeno e controlado. Em traces, atributos como feature.checkout_pix_v2=true ajudam a comparar latência do caminho antigo e novo.

O objetivo é conseguir olhar um dashboard durante o rollout e responder: erro 5xx subiu? Latência p95 mudou? A fila acumulou? O serviço externo recebeu mais chamadas? O novo caminho aumenta custo? Sem essa visibilidade, feature flag vira apenas um botão de esperança.

Rollout seguro na prática

Um plano simples para mudança média pode ser:

  1. Deploy com a flag desligada para todos.
  2. Ativação para ambiente interno ou conta de teste em produção.
  3. Ativação para 1% das contas por 30 a 60 minutos.
  4. Aumento para 5%, 25%, 50% e 100%, com janela de observação.
  5. Remoção da flag depois que o comportamento novo ficar estável.

Nem toda mudança precisa de tantos passos. Uma alteração pequena pode ir de interno para 100%. Uma mudança em pagamento, autorização, ranking, cobrança ou processamento assíncrono merece mais cuidado. Use risco, volume e reversibilidade para decidir.

Também defina rollback antes de começar. Quem pode desligar a flag? Onde está o painel ou arquivo? Quanto tempo leva para propagar? Existe cache local? O serviço novo aguenta ser desligado no meio de uma requisição ou job? Para workers, desligar a flag deve parar trabalho novo sem corromper o trabalho em andamento. O artigo de worker pool em Go ajuda nessa parte.

Flags e migrations

Flags não eliminam o cuidado com banco. Se o caminho novo depende de uma coluna nova, faça deploy em etapas compatíveis:

  1. Migration adiciona coluna ou tabela sem quebrar o código atual.
  2. Código novo escreve de forma compatível, ainda com flag desligada para leitura principal.
  3. Backfill preenche dados antigos.
  4. Flag ativa leitura nova gradualmente.
  5. Depois da estabilidade, o código antigo e colunas antigas podem ser removidos em outro ciclo.

Esse processo parece mais longo, mas evita o pior tipo de rollback: aquele em que o código antigo volta, mas o banco já perdeu a forma anterior. Para detalhes, leia migrations em Go para banco de dados e sqlc com PostgreSQL.

Cuidado com dívida de flags

O maior erro é deixar flags antigas acumularem. Cada flag permanente duplica caminhos mentais. Cada teste precisa considerar combinações. Cada incidente exige perguntar qual variação estava ativa. Depois de meses, o código fica cheio de galhos mortos.

Toda release flag deve nascer com data de revisão. Quando o rollout chega a 100% e fica estável, remova o caminho antigo, apague a configuração e simplifique o teste. Se a flag virou permissão de produto, renomeie e documente como regra permanente. Se virou kill switch operacional, escreva o procedimento de uso.

Outro erro é usar flag para esconder código ruim. Feature flag reduz blast radius, mas não substitui design, teste, review, observabilidade e rollback de deploy. Uma mudança perigosa continua perigosa; a flag apenas dá uma alavanca extra para controlar exposição.

Conexão com carreira e vagas Go

Feature flags aparecem cada vez mais em descrições de vagas backend, SRE, plataforma e liderança técnica. Empresas que fazem deploy frequente querem pessoas que entendam canary release, rollback, CI/CD, observabilidade, Kubernetes, migração compatível e métricas de produto. Saber explicar esse padrão mostra maturidade além da sintaxe de Go.

Se você está se preparando para entrevistas, combine este tema com perguntas de entrevista Go, currículo de desenvolvedor Go e vagas Go no Brasil. Para comparar como o mercado brasileiro pede práticas de entrega em outras stacks, o portal eu.dev.br reúne vagas de tecnologia no Brasil e ajuda a enxergar padrões comuns fora do nicho Go.

Checklist final

Antes de considerar uma feature flag pronta para produção, confirme:

  • A flag tem nome claro, dono e descrição.
  • O caminho antigo e o caminho novo têm testes.
  • A segmentação é estável por usuário, conta ou outro identificador correto.
  • Logs, métricas ou traces indicam qual variação foi usada.
  • Existe plano de rollout por etapas.
  • Existe plano de rollback sem novo deploy.
  • Migrations e dados são compatíveis com os dois caminhos.
  • A flag tem data de remoção ou motivo para ser permanente.

Feature flags em Go não precisam começar grandes. Comece com interface pequena, configuração explícita, testes bons e observabilidade suficiente. O ganho vem da disciplina: deploy deixa de ser aposta única, lançamento vira processo gradual e rollback passa a ser uma decisão operacional rápida. Em serviços Go de produção, essa diferença reduz incidentes e aumenta confiança para entregar software com frequência.