08 de maio de 2025 • 17 min de leitura
Desenvolvendo APIs Escaláveis com Golang: Dicas e Práticas para Construir Serviços Robustos
Aprenda a construir APIs de alta performance e confiáveis com Golang. Cobrimos concorrência, frameworks, bancos de dados, observability e mais para garantir escalabilidade e robustez em seus serviços backend.
Introdução
Golang, ou Go, tem ganhado imensa popularidade no desenvolvimento de serviços backend, especialmente APIs. Sua simplicidade, performance, modelo de concorrência robusto e compilação para binários estáticos a tornam uma escolha excelente para construir sistemas que precisam escalar e ser confiáveis. Neste artigo, exploraremos dicas e práticas essenciais para desenvolver APIs escaláveis e robustas com Go.
Por que Golang para APIs Escaláveis?
Antes de mergulharmos nas práticas, vamos entender rapidamente por que Go se destaca:
- Concorrência Nativa: Goroutines e channels facilitam a escrita de código concorrente de forma eficiente e menos propensa a erros comuns em outras linguagens.
- Performance: Go é compilada para código de máquina, resultando em execução rápida, próxima a linguagens como C/C++. Seu garbage collector é otimizado para baixa latência.
- Biblioteca Padrão Rica: O pacote
net/http
é poderoso e suficiente para construir APIs web sem a necessidade de frameworks pesados, embora existam ótimas opções. - Tipagem Estática: Ajuda a pegar erros em tempo de compilação, aumentando a robustez do código.
- Simplicidade: A sintaxe de Go é concisa e fácil de aprender, o que melhora a produtividade e a manutenção do código.
- Deployment Facilitado: Binários únicos e estaticamente vinculados simplificam o processo de deploy, especialmente em ambientes de contêineres.
Princípios de Design para Escalabilidade e Robustez
1. Statelessness (Ausência de Estado)
Serviços stateless são mais fáceis de escalar horizontalmente. Cada requisição deve conter toda a informação necessária para ser processada, sem depender de estado armazenado na instância do servidor que a atendeu anteriormente.
- Prática: Evite armazenar estado da sessão na memória da aplicação. Utilize bancos de dados, caches distribuídos (como Redis) ou JWTs para gerenciar o estado da sessão.
2. Graceful Shutdown
Sua API deve ser capaz de encerrar de forma graciosa, finalizando requisições em andamento e liberando recursos antes de parar. Isso é crucial durante deploys ou scaling down.
- Prática: Use sinais do sistema operacional (como
SIGINT
eSIGTERM
) para iniciar o processo de shutdown. A partir do Go 1.16, você pode utilizarsignal.NotifyContext()
para uma abordagem mais moderna.
package main
import (
"context"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second) // Simula trabalho
w.Write([]byte("Olá, Mundo!"))
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Cria um contexto cancelável com signal.NotifyContext (Go 1.16+)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop() // Garante que os recursos de NotifyContext sejam liberados
// Goroutine para iniciar o servidor
go func() {
log.Println("Servidor iniciado na porta 8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Erro ao iniciar servidor: %v", err)
}
}()
// Bloqueia até receber um sinal que cancele o contexto
<-ctx.Done()
stop() // Impede o recebimento de novos sinais após o início do shutdown
log.Println("Recebido sinal de shutdown. Encerrando servidor...")
// Cria um novo contexto para o shutdown com timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("Erro no graceful shutdown: %v", err)
}
log.Println("Servidor encerrado graciosamente.")
}
3. Tratamento de Erros Consistente
Go tem um tratamento de erros explícito. Use-o a seu favor para criar APIs robustas.
- Prática:
- Nunca ignore erros.
- Retorne erros significativos e, se necessário, envolva-os com contexto adicional usando
fmt.Errorf
ou o pacoteerrors
(a partir do Go 1.13, que introduziuerrors.Is
eerrors.As
). - Defina tipos de erro customizados para erros de domínio específicos.
- Padronize as respostas de erro da API (ex: JSON com campos
error
emessage
).
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
)
// Erro customizado
var ErrRecursoNaoEncontrado = errors.New("recurso não encontrado")
type apiError struct {
Status int `json:"status"`
Error string `json:"error"`
Message string `json:"message,omitempty"`
}
func responderComErro(w http.ResponseWriter, status int, err error, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(apiError{
Status: status,
Error: err.Error(),
Message: message,
})
}
func getItemHandler(w http.ResponseWriter, r *http.Request) {
itemID := r.URL.Query().Get("id")
if itemID == "" {
responderComErro(w, http.StatusBadRequest, errors.New("parâmetro 'id' ausente"), "")
return
}
// Simula busca no banco de dados
item, err := buscarItem(itemID)
if err != nil {
if errors.Is(err, ErrRecursoNaoEncontrado) {
responderComErro(w, http.StatusNotFound, err, "")
} else {
// Erro interno, não expor detalhes
log.Printf("Erro ao buscar item %s: %v", itemID, err)
responderComErro(w, http.StatusInternalServerError, errors.New("erro interno do servidor"), "")
}
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(item)
}
// Função simulada
func buscarItem(id string) (string, error) {
if id == "123" {
return "Item encontrado", nil
}
if id == "erro_db" {
return "", fmt.Errorf("falha na conexão com o banco: %w", errors.New("timeout"))
}
return "", ErrRecursoNaoEncontrado
}
// func main() { /* ... (necessário para rodar os handlers) ... */ }
4. Rate Limiting e Circuit Breakers
Para APIs robustas, é essencial implementar mecanismos que protejam seu serviço contra sobrecarga ou chamadas excessivas.
- Rate Limiting: Limite o número de requisições que um cliente pode fazer em determinado período.
- Circuit Breakers: Previna falhas em cascata ao detectar quando serviços dependentes estão com problemas.
package main
import (
"net/http"
"sync" // Adicionado para o Mutex
"time"
"golang.org/x/time/rate"
)
// Middleware de rate limiting simples por cliente (IP).
// Em produção, considere identificadores mais robustos e soluções distribuídas.
func rateLimitMiddleware(next http.Handler) http.Handler {
var mu sync.Mutex
limiters := make(map[string]*rate.Limiter)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Use r.Header.Get("X-Forwarded-For") ou similar se estiver atrás de um proxy.
// Cuidado com a confiabilidade desses headers.
clientIP := r.RemoteAddr
mu.Lock()
limiter, exists := limiters[clientIP]
if !exists {
// Exemplo: 10 requisições por segundo com burst de 20 por cliente
limiter = rate.NewLimiter(rate.Limit(10), 20)
limiters[clientIP] = limiter
}
mu.Unlock()
if !limiter.Allow() {
http.Error(w, "Limite de requisições excedido. Tente novamente mais tarde.", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
// Para circuit breaking, considere bibliotecas como sony/gobreaker, go-circuitbreaker ou hystrix-go (embora este último esteja em modo de manutenção).
// Alternativamente, soluções baseadas em service mesh (Istio, Linkerd) podem fornecer essa funcionalidade.
Aproveitando Concorrência em Go
1. Goroutines e Channels
Use goroutines para executar operações I/O-bound (chamadas de rede, acesso a banco de dados) de forma concorrente, sem bloquear o thread principal. Channels são usados para comunicação e sincronização entre goroutines.
- Prática: Use um
sync.WaitGroup
para esperar que múltiplas goroutines terminem antes de prosseguir.
2. Worker Pools
Para tarefas que podem ser processadas em paralelo e que chegam em grande volume, um worker pool pode ajudar a limitar o número de goroutines concorrentes, evitando sobrecarga de recursos.
package main
import (
"fmt"
"sync"
"time"
)
type Job struct {
ID int
Payload string
}
type Result struct {
JobID int
Output string
Err error
}
func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processando Job %d\n", id, job.ID)
time.Sleep(time.Second) // Simula trabalho
results <- Result{JobID: job.ID, Output: fmt.Sprintf("Resultado do Job %s", job.Payload)}
}
}
func main_workerpool() { // Renomeado para evitar conflito com outros mains
numJobs := 10
numWorkers := 3
jobs := make(chan Job, numJobs)
results := make(chan Result, numJobs)
var wg sync.WaitGroup
// Inicia os workers
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Envia os jobs
for j := 1; j <= numJobs; j++ {
jobs <- Job{ID: j, Payload: fmt.Sprintf("Payload-%d", j)}
}
close(jobs) // Fecha o canal de jobs para sinalizar que não há mais jobs
// Espera todos os workers terminarem
wg.Wait()
close(results) // Fecha o canal de resultados após todos os workers terminarem
// Coleta os resultados
for result := range results {
if result.Err != nil {
fmt.Printf("Erro no Job %d: %v\n", result.JobID, result.Err)
} else {
fmt.Printf("Job %d finalizado: %s\n", result.JobID, result.Output)
}
}
fmt.Println("Todos os jobs processados.")
}
Para aplicações mais complexas, considere usar bibliotecas estabelecidas:
- WorkerPool: Biblioteca leve e bem testada para criação de pools de workers.
- Pond: Pool de goroutines de alto desempenho e fácil utilização.
Escolhendo o Framework Certo (ou Nenhum)
-
net/http
(Biblioteca Padrão):- Prós: Sem dependências externas, controle total, ótimo para APIs simples ou para quem quer entender os fundamentos.
- Contras: Mais verboso para rotas complexas, middlewares, validação de requisições.
-
Frameworks (Gin, Echo, Chi, etc.):
- Prós: Abstrações úteis para roteamento, middleware, data binding, validação, rendering. Aceleram o desenvolvimento.
- Contras: Adicionam dependências, podem ter uma curva de aprendizado.
- Gin Exemplo:
package main import ( "context" "log" "net/http" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" ) func main_gin() { // Renomeado para evitar conflito r := gin.Default() // Default já inclui middleware de logging e recovery r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) // Exemplo de rota com parâmetro r.GET("/user/:name", func(c *gin.Context) { name := c.Param("name") c.String(http.StatusOK, "Olá %s!", name) }) // Configura o servidor HTTP srv := &http.Server{ Addr: ":8080", Handler: r, } // Graceful shutdown ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() // Inicia o servidor em uma goroutine go func() { log.Println("Servidor Gin iniciado na porta 8080") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Erro ao iniciar servidor Gin: %v", err) } }() // Espera pelo sinal de interrupção <-ctx.Done() stop() log.Println("Servidor Gin sendo desligado...") // Dá um tempo para o servidor finalizar requisições pendentes shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Fatalf("Erro ao desligar servidor Gin: %v", err) } log.Println("Servidor Gin encerrado com sucesso") }
Para rodar:
go get github.com/gin-gonic/gin
e depoisgo run seu_arquivo.go
(após definir qualmain
usar).
Interagindo com Bancos de Dados
- Connection Pooling: O pacote
database/sql
já gerencia um pool de conexões. ConfigureSetMaxOpenConns
,SetMaxIdleConns
,SetConnMaxLifetime
eSetConnMaxIdleTime
adequadamente. - Context para Timeouts e Cancelamento: Sempre passe um
context.Context
para as operações de banco de dados. Isso permite cancelar queries longas ou definir timeouts.
package main
import (
"context"
"database/sql"
"errors" // Adicionado para ErrRecursoNaoEncontrado (se não definido globalmente)
"fmt"
"log"
"time"
_ "github.com/lib/pq" // Driver do PostgreSQL, por exemplo
)
var db *sql.DB
// var ErrRecursoNaoEncontrado = errors.New("recurso não encontrado") // Defina se não estiver no escopo
func initDB() {
// String de conexão (exemplo para PostgreSQL)
// Substitua com suas credenciais e detalhes do banco
connStr := "user=seu_usuario password=sua_senha dbname=seu_banco host=localhost port=5432 sslmode=disable"
var err error
db, err = sql.Open("postgres", connStr)
if err != nil {
log.Fatalf("Erro ao conectar ao banco: %v", err)
}
// Configurações de pool de conexões
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(2 * time.Minute) // Bom para fechar conexões ociosas e evitar problemas de firewall
// Testa a conexão
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err = db.PingContext(ctx); err != nil {
log.Fatalf("Erro ao pingar o banco: %v", err)
}
log.Println("Conectado ao banco de dados!")
}
type User struct {
ID int
Name string
}
func getUserByID(ctx context.Context, userID int) (*User, error) {
queryCtx, cancel := context.WithTimeout(ctx, 3*time.Second) // Timeout específico para esta query
defer cancel()
row := db.QueryRowContext(queryCtx, "SELECT id, name FROM users WHERE id = $1", userID)
var user User
if err := row.Scan(&user.ID, &user.Name); err != nil {
if errors.Is(err, sql.ErrNoRows) { // Use errors.Is para comparar com sql.ErrNoRows
return nil, fmt.Errorf("usuário com ID %d não encontrado: %w", userID, ErrRecursoNaoEncontrado)
}
return nil, fmt.Errorf("erro ao buscar usuário %d: %w", userID, err)
}
return &user, nil
}
Nota: Para rodar este exemplo, você precisa ter um PostgreSQL rodando e a tabela users
criada.
Middleware Essenciais
Middlewares são funções que processam requisições antes ou depois do handler principal.
- Logging: Registre informações importantes sobre cada requisição (método, path, status code, latência).
- Recovery: Capture panics e retorne um erro 500 HTTP em vez de derrubar o servidor.
- Autenticação/Autorização: Verifique credenciais (ex: JWTs, API Keys) e permissões.
- Rate Limiting: Proteja sua API contra abuso, limitando o número de requisições por cliente.
- CORS: Configure Cross-Origin Resource Sharing corretamente para aplicações web.
- Exemplo (Gin):
// package main // Supondo que está no mesmo contexto do exemplo Gin anterior import ( "net/http" "github.com/gin-gonic/gin" ) // Middleware de autenticação customizado (exemplo básico) func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { apiKey := c.GetHeader("X-API-KEY") if apiKey != "minha-chave-secreta" { // Lógica de autenticação real aqui c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Não autorizado"}) return } c.Next() } } func setupRouterWithMiddlewares() *gin.Engine { r := gin.Default() // gin.Default() já inclui Logger e Recovery // Middleware de CORS // Para mais controle, use [github.com/gin-contrib/cors](https://github.com/gin-contrib/cors) r.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // Em produção, especifique origens permitidas c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-KEY") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) return } c.Next() }) r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "pong"}) }) authorized := r.Group("/admin", AuthMiddleware()) authorized.GET("/dashboard", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "Área administrativa"}) }) return r } // func main() { // router := setupRouterWithMiddlewares() // router.Run(":8080") // }
- Exemplo (Gin):
Observabilidade
Para manter APIs escaláveis e robustas, é crucial poder observar seu comportamento.
-
Logging Estruturado: Use logs em formato JSON (ex: com
log/slog
,zap
ouzerolog
) para facilitar a análise e busca. Inclua IDs de correlação para rastrear requisições através de múltiplos serviços.- slog (a partir do Go 1.21): O novo pacote da biblioteca padrão para logging estruturado.
- zap: Biblioteca de alta performance da Uber, excelente para aplicações que priorizam performance.
- zerolog: Extremamente rápido, com alocações zero em muitos casos de uso.
package main import ( "log/slog" "os" ) func main_slog() { // Renomeado // Configurar o logger com saída em JSON handlerOpts := &slog.HandlerOptions{ AddSource: true, // Adiciona source:file:line ao log Level: slog.LevelDebug, // Define o nível mínimo de log } logger := slog.New(slog.NewJSONHandler(os.Stdout, handlerOpts)) slog.SetDefault(logger) // Define como logger global // Logs estruturados slog.Info("Servidor iniciado", "porta", 8080, "ambiente", "produção", slog.String("versão", "1.0.0")) // Usar slog.String para tipagem explícita // Log com grupo de campos slog.Info("Requisição recebida", slog.Group("http", slog.String("método", "GET"), slog.String("path", "/api/users"), slog.Int("status", 200), slog.Duration("latência", 42 * time.Millisecond), // Usar slog.Duration ), slog.Group("cliente", slog.String("ip", "192.168.1.1"), slog.String("user_agent", "Mozilla/5.0..."), )) }
-
Métricas: Exponha métricas sobre a saúde da aplicação (taxa de requisições, taxa de erros, latência, uso de recursos). Prometheus com
promhttp
é uma escolha popular.package main import ( "log" // Adicionado para o log.Println "net/http" "strconv" // Adicionado para strconv.Itoa "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( // Contador de requisições HTTP por método, path e código de status httpRequestsTotal = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total de requisições HTTP por método, path e status.", }, []string{"method", "path", "status"}, ) // Histograma para medir latência das requisições httpRequestDuration = promauto.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "Duração das requisições HTTP em segundos.", Buckets: prometheus.DefBuckets, // Ou buckets customizados }, []string{"method", "path", "status"}, ) ) // responseWriterWrapper é um wrapper em torno de http.ResponseWriter // para capturar o código de status da resposta. type responseWriterWrapper struct { http.ResponseWriter statusCode int } func newResponseWriterWrapper(w http.ResponseWriter) *responseWriterWrapper { // Default para 200 OK se WriteHeader não for chamado. return &responseWriterWrapper{w, http.StatusOK} } func (rww *responseWriterWrapper) WriteHeader(statusCode int) { rww.statusCode = statusCode rww.ResponseWriter.WriteHeader(statusCode) } // Middleware para registrar métricas func metricsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() wrappedWriter := newResponseWriterWrapper(w) next.ServeHTTP(wrappedWriter, r) // Processa a requisição duration := time.Since(start).Seconds() statusStr := strconv.Itoa(wrappedWriter.statusCode) // Converte int para string httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, statusStr).Inc() httpRequestDuration.WithLabelValues(r.Method, r.URL.Path, statusStr).Observe(duration) }) }
-
Tracing Distribuído: Em arquiteturas de microserviços, use OpenTelemetry para entender o fluxo de uma requisição através de diferentes serviços e identificar gargalos.
package main import ( "context" "log" "net/http" "time" // Adicionado para o exemplo de processarNegocio "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" // Exemplo com gRPC exporter "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.24.0" // Verifique a versão mais recente "go.opentelemetry.io/otel/trace" // Para usar um exporter diferente, como stdouttrace: // "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" ) var tracer trace.Tracer // Global tracer // initTracer configura e registra o OpenTelemetry Global TracerProvider. // Retorna uma função de shutdown para limpar recursos. func initTracer() (func(context.Context) error, error) { ctx := context.Background() // Crie um exportador OTLP via gRPC. Ajuste o endpoint conforme necessário. // Para produção, configure `otlptracegrpc.WithEndpoint("your-otel-collector:4317")` e `otlptracegrpc.WithInsecure()` // ou com TLS: `otlptracegrpc.WithTLSCredentials(...)`. exporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint("localhost:4317")) if err != nil { return nil, fmt.Errorf("falha ao criar exportador OTLP gRPC: %w", err) } // Exemplo alternativo: Exportador para stdout (útil para debug) // exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) // if err != nil { // return nil, fmt.Errorf("failed to create stdout exporter: %w", err) // } res, err := resource.New(ctx, resource.WithAttributes( semconv.SchemaURL, semconv.ServiceName("minha-api-golang"), semconv.ServiceVersion("v0.1.0"), attribute.String("environment", "desenvolvimento"), ), ) if err != nil { return nil, fmt.Errorf("falha ao criar recurso: %w", err) } // Cria um provider com o exportador em batch. tracerProvider := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), // Amostragem: Pode ser ParentBased(TraceIDRatioBased(0.1)) para amostrar 10% das traces sdktrace.WithSampler(sdktrace.AlwaysSample()), // Amostra todas as traces (não recomendado para produção) ) otel.SetTracerProvider(tracerProvider) // Define o provider global otel.SetTextMapPropagator(nil) // Use o propagador padrão (W3C Trace Context e Baggage) tracer = otel.Tracer("github.com/seuusuario/minha-api-golang") // Nome do Tracer, geralmente o nome do módulo/pacote return exporter.Shutdown, nil } func httpHandler(w http.ResponseWriter, r *http.Request) { // O contexto da requisição já pode conter um span pai (propagado) ctx := r.Context() var span trace.Span ctx, span = tracer.Start(ctx, "http.request.handler", // Nome do span trace.WithAttributes( semconv.HTTPMethodKey.String(r.Method), semconv.HTTPURLKey.String(r.URL.String()), semconv.NetPeerIPKey.String(r.RemoteAddr), ), trace.WithSpanKind(trace.SpanKindServer), // Indica que é um span de servidor ) defer span.End() // Garante que o span seja finalizado // Adicionar mais atributos span.SetAttributes(attribute.String("custom.info", "informação adicional")) // Executar lógica de negócio, potencialmente criando sub-spans result, err := processarNegocioComTrace(ctx) if err != nil { span.RecordError(err) // Registra o erro no span span.SetStatus(otel.GetCode(err), err.Error()) // Define o status do span como erro http.Error(w, "Erro interno do servidor", http.StatusInternalServerError) return } span.SetStatus(otel.GetCode(nil), "") // Sucesso w.Write([]byte(result)) } func processarNegocioComTrace(ctx context.Context) (string, error) { // Criar um span filho para a lógica de negócio ctx, span := tracer.Start(ctx, "processar.negocio.interno") defer span.End() // Simula trabalho time.Sleep(50 * time.Millisecond) span.AddEvent("Etapa 1 concluída", trace.WithAttributes(attribute.Bool("sucesso", true))) time.Sleep(100 * time.Millisecond) // Exemplo de erro // if true { // return "", errors.New("falha simulada no processamento de negócio") // } return "Sucesso do negócio", nil }
Testes
- Testes Unitários: Teste funções e pacotes isoladamente. Go tem um ótimo suporte nativo com o pacote
testing
. - Testes de Integração: Teste a interação entre componentes, como sua API e o banco de dados.
- Testes de API (End-to-End): Use o pacote
net/http/httptest
para testar seus handlers HTTP sem precisar subir um servidor real. - Testes de Carga: Ferramentas como k6, JMeter ou Artillery podem simular tráfego em grandes volumes para verificar o comportamento sob carga.
// Em um arquivo user_handlers_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
// "exemplo.com/seuprojeto/internal/handlers" // Supondo que o handler está aqui
// "exemplo.com/seuprojeto/internal/database" // Supondo que o mock do DB e Item estão aqui
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
// "github.com/stretchr/testify/mock" // Para mockar o banco
)
func setupRouterForTest() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New() // Usar New() em vez de Default() para testes mais limpos
return r
}
func TestPingRoute(t *testing.T) {
router := setupRouterForTest()
// Adicionando a rota de ping para este teste específico
router.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/ping", nil) // Usar http.MethodGet
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.JSONEq(t, `{"message":"pong"}`, w.Body.String())
}
Conclusão
Desenvolver APIs escaláveis e robustas com Golang é uma tarefa recompensadora. A linguagem oferece ferramentas poderosas para concorrência, performance e simplicidade. Ao aplicar os princípios de design como statelessness e graceful shutdown, escolher as ferramentas certas (seja a biblioteca padrão ou frameworks), gerenciar interações com banco de dados de forma eficiente, implementar middlewares essenciais e focar em observabilidade e testes, você estará no caminho certo para construir serviços que podem crescer e se adaptar às demandas do seu negócio.
Lembre-se que escalabilidade e robustez são jornadas contínuas. Monitore suas aplicações, aprenda com os incidentes e itere sobre suas soluções. Boa codificação!