Tecnologias/01 - Linguagens/01 - Go6 min
Go em Produção
Conceito
Go tem uma reputação de "só funciona" que é merecida no build e enganosa no runtime. As falhas de um serviço Go em produção são quase sempre as mesmas cinco, e nenhuma delas aparece em ambiente de desenvolvimento.
1. O binário e o contêiner
FROM golang:1.27 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /app ./cmd/api
FROM gcr.io/distroless/static-debian12 # ou scratch
COPY --from=build /app /app
USER 65532:65532
ENTRYPOINT ["/app"]
CGO_ENABLED=0é o que garante binário estático — sem isso você depende da libc da imagem base e descobre no deploy.distroless/staticem vez descratchquando você precisa de certificados TLS e/etc/passwd.- Cross-compile é
GOOS=linux GOARCH=arm64 go build. Sem toolchain extra.
2. CPU: GOMAXPROCS e o cgroup
O erro clássico: o contêiner tem limite de 1 CPU, a máquina tem 64 núcleos, e o runtime
antigo do Go definia GOMAXPROCS=64. Resultado: 64 threads brigando por 1 CPU de cota,
throttling do cgroup, latência p99 horrível — com o gráfico de CPU mostrando "só 40%
de uso".
Por anos a correção era a biblioteca automaxprocs da Uber. A partir do Go 1.25 o
runtime passou a considerar o limite de CPU do cgroup ao definir GOMAXPROCS. Se você
está em Go 1.27, isso já vem resolvido — mas vale conferir, porque muitos serviços ainda
fixam a variável na mão.
3. Memória: GOMEMLIMIT é obrigatório em contêiner
O GC do Go decide coletar com base em crescimento relativo (GOGC=100 = coleta
quando o heap dobra). Ele não sabe que existe um limite de 512 MB no cgroup. Então:
heap em 300 MB → GC mira em 600 MB → limite é 512 MB → OOMKilled
GOMEMLIMIT (Go 1.19+) dá ao GC um limite flexível de memória total: chegando perto,
ele coleta mais agressivamente em vez de estourar.
env:
- name: GOMEMLIMIT
value: "450MiB" # ~85–90% do limite do contêiner
Deixe folga: GOMEMLIMIT cobre o que o runtime gerencia, não stacks de thread do SO nem
memória de cgo.
4. O pool de conexões: o bug de produção nº 1
Este é o encontro entre esta seção e a de bancos. O
database/sql não é um driver, é um pool — e os padrões dele são hostis:
db.SetMaxOpenConns(25) // padrão: ILIMITADO
db.SetMaxIdleConns(25) // padrão: 2 (causa churn de conexão)
db.SetConnMaxLifetime(30 * time.Minute) // padrão: infinito (não sobrevive a failover)
db.SetConnMaxIdleTime(5 * time.Minute)
Junte "goroutine é barata" (2. Goroutines e Channels) com "MaxOpenConns é ilimitado"
e você tem a receita exata do incidente: um pico de tráfego abre 3.000 conexões contra um
PostgreSQL que aceita 100 — e como no Postgres cada conexão é um
processo, a máquina do banco cai antes de recusar. Com MaxOpenConns definido, o
excesso espera no pool: degradação em vez de queda. É Queue-Based Load Leveling
aplicado ao driver.
ConnMaxLifetime é o que faz o serviço sobreviver a um failover: sem ele, o pool segura
conexões para um primário que já morreu.
5. Diagnóstico: pprof
import _ "net/http/pprof" // registra em /debug/pprof no DefaultServeMux
/debug/pprof na porta públicaSuba um segundo http.Server numa porta interna. pprof entrega goroutine dumps e
heap — com nomes de função, argumentos e às vezes dados.
| Perfil | Responde |
|---|---|
profile (CPU) |
onde o tempo de CPU vai |
heap |
quem está alocando e retendo |
allocs |
total alocado desde o início (mesmo já liberado) |
goroutine |
quantas e paradas onde |
goroutineleak |
quais estão permanentemente bloqueadas (Go 1.27) |
block / mutex |
contenção de channel e de lock |
trace |
scheduler, GC e latência no tempo |
go tool pprof -http=: http://localhost:6060/debug/pprof/heap
curl -o t.out 'http://localhost:6060/debug/pprof/trace?seconds=5' && go tool trace t.out
Trade-offs
O GC do Go é otimizado para latência, não para throughput. Pausas de sub-milissegundo
são a promessa cumprida; o custo é mais CPU gasto em coleta e um footprint de memória
maior que o mínimo teórico. Não há como escolher outro GC — o único botão real é
alocar menos (reuso via sync.Pool, strings.Builder, pré-alocar slices com
make([]T, 0, n), evitar boxing em interface{}).
Binário estático facilita o deploy e engorda a imagem. Um "hello world" HTTP dá ~8–10 MB. É o preço de não ter runtime instalado.
Observabilidade é sua responsabilidade. Não há agente automático. Você instrumenta
com log/slog, OpenTelemetry e expvar — o que dá controle e trabalho.
O trade-off central: Go entrega excelentes padrões de linguagem e péssimos padrões de operação.
MaxOpenConnsilimitado,ConnMaxLifetimeinfinito e GC sem noção de cgroup são todos padrões que só funcionam fora de contêiner. Em produção, a configuração explícita não é otimização — é requisito.
Exemplo prático
Um main que erra menos: timeouts, pool, shutdown limpo e porta de diagnóstico
separada.
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil { log.Fatal(err) }
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(30 * time.Minute)
defer db.Close()
// verifique de fato: sql.Open não conecta, só valida a string
pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := db.PingContext(pingCtx); err != nil { log.Fatal(err) }
// porta interna: métricas e pprof, fora do tráfego público
go func() { _ = http.ListenAndServe("127.0.0.1:6060", nil) }()
srv := &http.Server{
Addr: ":8080",
Handler: rotas(db),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("listen", "err", err)
}
}()
<-ctx.Done() // SIGTERM do orquestrador
slog.Info("drenando conexões")
shutCtx, cancel2 := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel2()
_ = srv.Shutdown(shutCtx) // termina requisições em voo; para de aceitar novas
}
Shutdown e o terminationGracePeriodSecondsO timeout do Shutdown precisa ser menor que o período de graça do orquestrador.
Se o Kubernetes espera 30s e você drena por 60s, o SIGKILL chega no meio e a
requisição do cliente morre de qualquer forma. Ver Fail-Over.
Checklist de produção
-
CGO_ENABLED=0, imagem distroless, usuário não-root -
GOMEMLIMIT~85% do limite de memória do contêiner - Todos os timeouts do
http.Serverdefinidos (o padrão é sem timeout) -
context.WithTimeoutem toda chamada de saída (banco, HTTP, Kafka) - Pool do
database/sqlconfigurado nos 4 parâmetros -
pprofem porta interna;goroutineleakno runbook - Shutdown gracioso ligado ao
SIGTERM -
go test -raceegovulncheckna CI
Aprofundamento aplicado
Orçamento de tempo de uma requisição
Suponha um prazo total de 800 ms: a requisição espera no pool, consulta o banco e chama um serviço externo. Dar 800 ms novos a cada etapa permite ultrapassar o prazo total. Propague o contexto original e, quando útil, crie prazos menores para etapas específicas.
Uma consulta rápida depois de esperar 700 ms por uma conexão continua sendo uma requisição lenta. Registre separadamente espera no pool, execução e serialização. O número máximo de conexões deve considerar todas as réplicas da aplicação: 20 instâncias com limite de 30 podem disputar até 600 conexões.
Encerrar sem perder trabalho em andamento
- Retire a instância da recepção de novas requisições conforme o balanceador.
- Inicie a drenagem do servidor e avise workers de longa duração.
- Aguarde operações aceitas dentro de um prazo total.
- Encerre dependências depois que seus usuários terminarem.
O Shutdown do servidor HTTP não gerencia automaticamente todos os workers próprios
nem conexões sequestradas pelo handler; esses recursos precisam de ciclo de vida explícito.
Referência: Server.Shutdown.
Exercício: envie sinal de encerramento durante uma chamada lenta. Resultado esperado: a requisição termina dentro da janela planejada ou recebe cancelamento observável; o banco não é fechado enquanto ainda está em uso.
Relacionado
- 2. Goroutines e Channels
- 4. Versões, Módulos e Compatibilidade
- 1. PostgreSQL
- Queue-Based Load Leveling
- Fail-Over
- Improper Instantiation
Parte de MOC Go · go.dev/doc/diagnostics