Selfie de Miguel no espelho, com visual estiloso, tattoos à mostra e camiseta com arte gráfica.

Miguel MachadoSoftware Engineer

Apaixonado por desenvolvimento web, backend e games. Trabalhando com TypeScript, React, Next.js, Golang, e mais.

← Voltar ao início

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.

#golang#api#escalabilidade#microserviços#desenvolvimento backend#robustez

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:

  1. 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.
  2. 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.
  3. 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.
  4. Tipagem Estática: Ajuda a pegar erros em tempo de compilação, aumentando a robustez do código.
  5. Simplicidade: A sintaxe de Go é concisa e fácil de aprender, o que melhora a produtividade e a manutenção do código.
  6. 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 e SIGTERM) para iniciar o processo de shutdown. A partir do Go 1.16, você pode utilizar signal.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 pacote errors (a partir do Go 1.13, que introduziu errors.Is e errors.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 e message).
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)

  1. 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.
  2. 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 depois go run seu_arquivo.go (após definir qual main usar).

Interagindo com Bancos de Dados

  1. Connection Pooling: O pacote database/sql já gerencia um pool de conexões. Configure SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime e SetConnMaxIdleTime adequadamente.
  2. 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.

  1. Logging: Registre informações importantes sobre cada requisição (método, path, status code, latência).
  2. Recovery: Capture panics e retorne um erro 500 HTTP em vez de derrubar o servidor.
  3. Autenticação/Autorização: Verifique credenciais (ex: JWTs, API Keys) e permissões.
  4. Rate Limiting: Proteja sua API contra abuso, limitando o número de requisições por cliente.
  5. 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")
      // }
      

Observabilidade

Para manter APIs escaláveis e robustas, é crucial poder observar seu comportamento.

  1. Logging Estruturado: Use logs em formato JSON (ex: com log/slog, zap ou zerolog) 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..."),
            ))
    }
    
  2. 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)
        })
    }
    
  3. 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

  1. Testes Unitários: Teste funções e pacotes isoladamente. Go tem um ótimo suporte nativo com o pacote testing.
  2. Testes de Integração: Teste a interação entre componentes, como sua API e o banco de dados.
  3. Testes de API (End-to-End): Use o pacote net/http/httptest para testar seus handlers HTTP sem precisar subir um servidor real.
  4. 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!


Compartilhar: