Go é uma das melhores linguagens para desenvolvimento de APIs REST. Com sua performance nativa, concorrência eficiente via goroutines e biblioteca padrão robusta, Go permite criar serviços web escaláveis e de alta performance. Neste guia completo, vamos construir uma API REST do zero, cobrindo desde conceitos básicos até padrões avançados usados em produção.

Por Que Go para APIs REST?

Vantagens de Go para Backend

┌─────────────────────────────────────────────────────────┐
│  PERFORMANCE          │  Goroutines leves (2KB stack)   │
├─────────────────────────────────────────────────────────┤
│  CONFIABILIDADE       │  Tipagem forte, erro explícito  │
├─────────────────────────────────────────────────────────┤
│  PRODUTIVIDADE        │  Compilação rápida, stdlib rica │
├─────────────────────────────────────────────────────────┤
│  DEPLOY               │  Binário único, cross-compile   │
├─────────────────────────────────────────────────────────┤
│  ECOSISTEMA           │  Gin, Echo, Chi, Fiber          │
└─────────────────────────────────────────────────────────┘

Empresas usando Go em produção:

  • Google (Kubernetes, Docker)
  • Uber (microservices)
  • Netflix (backend services)
  • Dropbox (storage systems)
  • Mercado Libre (pagamentos)

Servidor HTTP Básico

Hello World Web

Vamos começar com o servidor HTTP mais simples em Go:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	// Handler para rota /
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Olá, Mundo!")
	})

	// Iniciar servidor na porta 8080
	log.Println("Servidor rodando em http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Testando:

go run main.go
curl http://localhost:8080
# Output: Olá, Mundo!

Múltiplas Rotas com Standard Library

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"time"
)

// Resposta padrão da API
type Response struct {
	Message string    `json:"message"`
	Time    time.Time `json:"time"`
}

func main() {
	// Configurar rotas
	mux := http.NewServeMux()
	
	mux.HandleFunc("GET /", homeHandler)
	mux.HandleFunc("GET /health", healthHandler)
	mux.HandleFunc("GET /api/status", statusHandler)

	// Configurar servidor com timeouts
	server := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		IdleTimeout:  60 * time.Second,
	}

	log.Println("API em http://localhost:8080")
	log.Fatal(server.ListenAndServe())
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(Response{
		Message: "Bem-vindo à API Go!",
		Time:    time.Now(),
	})
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(map[string]string{
		"status": "healthy",
	})
}

func statusHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"service":   "api-go",
		"version":   "1.0.0",
		"timestamp": time.Now().Unix(),
	})
}

Roteamento com Gorilla Mux

Por Que Usar um Router?

A biblioteca padrão é boa, mas routers como Gorilla Mux oferecem:

  • Path parameters: /users/{id}
  • Method matching: GET, POST, etc.
  • Middleware chains
  • Subrouters

Instalação e Uso Básico

go get -u github.com/gorilla/mux
package main

import (
	"encoding/json"
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/gorilla/mux"
)

// Modelo de Usuário
type User struct {
	ID        uint      `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

// "Banco de dados" em memória
var users = []User{
	{ID: 1, Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
	{ID: 2, Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
}
var nextID uint = 3

func main() {
	r := mux.NewRouter()

	// Rotas da API
	api := r.PathPrefix("/api/v1").Subrouter()
	
	// Users
	api.HandleFunc("/users", getUsers).Methods("GET")
	api.HandleFunc("/users", createUser).Methods("POST")
	api.HandleFunc("/users/{id}", getUser).Methods("GET")
	api.HandleFunc("/users/{id}", updateUser).Methods("PUT")
	api.HandleFunc("/users/{id}", deleteUser).Methods("DELETE")

	// Middleware
	r.Use(loggingMiddleware)
	r.Use(contentTypeMiddleware)

	server := &http.Server{
		Addr:         ":8080",
		Handler:      r,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
	}

	log.Println("API em http://localhost:8080")
	log.Fatal(server.ListenAndServe())
}

// Middleware de logging
func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
	})
}

// Middleware de content-type
func contentTypeMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		next.ServeHTTP(w, r)
	})
}

// Handlers
func getUsers(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(users)
}

func getUser(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.ParseUint(vars["id"], 10, 32)
	if err != nil {
		http.Error(w, `{"error": "ID inválido"}`, http.StatusBadRequest)
		return
	}

	for _, user := range users {
		if user.ID == uint(id) {
			json.NewEncoder(w).Encode(user)
			return
		}
	}

	http.Error(w, `{"error": "Usuário não encontrado"}`, http.StatusNotFound)
}

func createUser(w http.ResponseWriter, r *http.Request) {
	var user User
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		http.Error(w, `{"error": "JSON inválido"}`, http.StatusBadRequest)
		return
	}

	// Validação simples
	if user.Name == "" || user.Email == "" {
		http.Error(w, `{"error": "Nome e email são obrigatórios"}`, http.StatusBadRequest)
		return
	}

	user.ID = nextID
	nextID++
	user.CreatedAt = time.Now()
	users = append(users, user)

	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(user)
}

func updateUser(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, _ := strconv.ParseUint(vars["id"], 10, 32)

	var updated User
	if err := json.NewDecoder(r.Body).Decode(&updated); err != nil {
		http.Error(w, `{"error": "JSON inválido"}`, http.StatusBadRequest)
		return
	}

	for i, user := range users {
		if user.ID == uint(id) {
			users[i].Name = updated.Name
			users[i].Email = updated.Email
			json.NewEncoder(w).Encode(users[i])
			return
		}
	}

	http.Error(w, `{"error": "Usuário não encontrado"}`, http.StatusNotFound)
}

func deleteUser(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, _ := strconv.ParseUint(vars["id"], 10, 32)

	for i, user := range users {
		if user.ID == uint(id) {
			users = append(users[:i], users[i+1:]...)
			w.WriteHeader(http.StatusNoContent)
			return
		}
	}

	http.Error(w, `{"error": "Usuário não encontrado"}`, http.StatusNotFound)
}

Middleware Avançado

Autenticação JWT

package middleware

import (
	"context"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("sua-chave-secreta-aqui") // Use variável de ambiente!

// Claims personalizadas
type Claims struct {
	UserID uint   `json:"user_id"`
	Email  string `json:"email"`
	jwt.RegisteredClaims
}

// Gerar token
func GenerateToken(userID uint, email string) (string, error) {
	claims := Claims{
		UserID: userID,
		Email:  email,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString(jwtSecret)
}

// Middleware de autenticação
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		authHeader := r.Header.Get("Authorization")
		if authHeader == "" {
			http.Error(w, `{"error": "Token não fornecido"}`, http.StatusUnauthorized)
			return
		}

		// Extrair token (Bearer token)
		parts := strings.Split(authHeader, " ")
		if len(parts) != 2 || parts[0] != "Bearer" {
			http.Error(w, `{"error": "Formato de token inválido"}`, http.StatusUnauthorized)
			return
		}

		tokenString := parts[1]

		// Validar token
		claims := &Claims{}
		token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
			return jwtSecret, nil
		})

		if err != nil || !token.Valid {
			http.Error(w, `{"error": "Token inválido"}`, http.StatusUnauthorized)
			return
		}

		// Adicionar claims ao contexto
		ctx := context.WithValue(r.Context(), "claims", claims)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// Recuperar usuário do contexto
func GetUserFromContext(r *http.Request) *Claims {
	claims, ok := r.Context().Value("claims").(*Claims)
	if !ok {
		return nil
	}
	return claims
}

Rate Limiting

package middleware

import (
	"net/http"
	"sync"
	"time"
)

// RateLimiter implementa token bucket
type RateLimiter struct {
	requests map[string][]time.Time
	limit    int
	window   time.Duration
	mu       sync.RWMutex
}

func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
	rl := &RateLimiter{
		requests: make(map[string][]time.Time),
		limit:    limit,
		window:   window,
	}
	
	// Limpar entradas antigas periodicamente
	go rl.cleanup()
	
	return rl
}

func (rl *RateLimiter) cleanup() {
	ticker := time.NewTicker(rl.window)
	for range ticker.C {
		rl.mu.Lock()
		now := time.Now()
		for ip, times := range rl.requests {
			var valid []time.Time
			for _, t := range times {
				if now.Sub(t) < rl.window {
					valid = append(valid, t)
				}
			}
			if len(valid) == 0 {
				delete(rl.requests, ip)
			} else {
				rl.requests[ip] = valid
			}
		}
		rl.mu.Unlock()
	}
}

func (rl *RateLimiter) Allow(ip string) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := time.Now()
	times := rl.requests[ip]

	// Remover requisições antigas
	var valid []time.Time
	for _, t := range times {
		if now.Sub(t) < rl.window {
			valid = append(valid, t)
		}
	}

	if len(valid) >= rl.limit {
		return false
	}

	rl.requests[ip] = append(valid, now)
	return true
}

func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ip := r.RemoteAddr
			if !limiter.Allow(ip) {
				w.WriteHeader(http.StatusTooManyRequests)
				json.NewEncoder(w).Encode(map[string]string{
					"error": "Rate limit exceeded",
				})
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

Integração com Banco de Dados

Usando SQL nativo

package database

import (
	"database/sql"
	"fmt"
	"log"
	"time"

	_ "github.com/mattn/go-sqlite3" // ou "github.com/lib/pq" para PostgreSQL
)

// DB encapsula a conexão
type DB struct {
	*sql.DB
}

func New(connectionString string) (*DB, error) {
	db, err := sql.Open("sqlite3", connectionString)
	if err != nil {
		return nil, err
	}

	// Configurar pool de conexões
	db.SetMaxOpenConns(25)
	db.SetMaxIdleConns(25)
	db.SetConnMaxLifetime(5 * time.Minute)

	// Verificar conexão
	if err := db.Ping(); err != nil {
		return nil, err
	}

	return &DB{db}, nil
}

// Criar tabelas
func (db *DB) Migrate() error {
	query := `
	CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		name TEXT NOT NULL,
		email TEXT UNIQUE NOT NULL,
		password_hash TEXT NOT NULL,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
		updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
	);

	CREATE TABLE IF NOT EXISTS posts (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		user_id INTEGER NOT NULL,
		title TEXT NOT NULL,
		content TEXT,
		created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
		FOREIGN KEY (user_id) REFERENCES users(id)
	);

	CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
	`

	_, err := db.Exec(query)
	return err
}

// Repository Pattern
type UserRepository struct {
	db *DB
}

func NewUserRepository(db *DB) *UserRepository {
	return &UserRepository{db: db}
}

func (r *UserRepository) Create(user *User) error {
	query := `
		INSERT INTO users (name, email, password_hash)
		VALUES (?, ?, ?)
		RETURNING id, created_at
	`
	return r.db.QueryRow(query, user.Name, user.Email, user.PasswordHash).
		Scan(&user.ID, &user.CreatedAt)
}

func (r *UserRepository) GetByID(id int64) (*User, error) {
	user := &User{}
	query := `SELECT id, name, email, created_at, updated_at FROM users WHERE id = ?`
	
	err := r.db.QueryRow(query, id).Scan(
		&user.ID, &user.Name, &user.Email,
		&user.CreatedAt, &user.UpdatedAt,
	)
	
	if err == sql.ErrNoRows {
		return nil, fmt.Errorf("usuário não encontrado")
	}
	
	return user, err
}

func (r *UserRepository) GetAll() ([]User, error) {
	query := `SELECT id, name, email, created_at, updated_at FROM users ORDER BY created_at DESC`
	
	rows, err := r.db.Query(query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var users []User
	for rows.Next() {
		var u User
		err := rows.Scan(
			&u.ID, &u.Name, &u.Email,
			&u.CreatedAt, &u.UpdatedAt,
		)
		if err != nil {
			return nil, err
		}
		users = append(users, u)
	}
	
	return users, rows.Err()
}

func (r *UserRepository) Update(user *User) error {
	query := `
		UPDATE users 
		SET name = ?, email = ?, updated_at = CURRENT_TIMESTAMP
		WHERE id = ?
	`
	result, err := r.db.Exec(query, user.Name, user.Email, user.ID)
	if err != nil {
		return err
	}

	rows, _ := result.RowsAffected()
	if rows == 0 {
		return fmt.Errorf("usuário não encontrado")
	}
	
	return nil
}

func (r *UserRepository) Delete(id int64) error {
	result, err := r.db.Exec("DELETE FROM users WHERE id = ?", id)
	if err != nil {
		return err
	}

	rows, _ := result.RowsAffected()
	if rows == 0 {
		return fmt.Errorf("usuário não encontrado")
	}
	
	return nil
}

Usando GORM (ORM)

package main

import (
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

type User struct {
	ID        uint           `gorm:"primarykey" json:"id"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
	Name      string         `gorm:"size:100;not null" json:"name"`
	Email     string         `gorm:"size:100;uniqueIndex;not null" json:"email"`
	Password  string         `gorm:"-" json:"password,omitempty"` // Ignorar no DB
	Posts     []Post         `json:"posts,omitempty"`
}

type Post struct {
	ID      uint   `gorm:"primarykey" json:"id"`
	Title   string `gorm:"size:200;not null" json:"title"`
	Content string `json:"content"`
	UserID  uint   `json:"user_id"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("app.db"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		log.Fatal(err)
	}

	// Auto migrate
	db.AutoMigrate(&User{}, &Post{})

	// CRUD com GORM
	// Create
	user := User{Name: "Alice", Email: "alice@example.com"}
	result := db.Create(&user)
	log.Printf("Usuário criado: ID=%d, erro=%v", user.ID, result.Error)

	// Read
	var found User
	db.First(&found, user.ID)
	
	// Update
	db.Model(&found).Update("name", "Alice Silva")
	
	// Delete
	db.Delete(&found)
}

Testando APIs

Testes Unitários

package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/gorilla/mux"
)

func TestGetUsers(t *testing.T) {
	req, err := http.NewRequest("GET", "/api/v1/users", nil)
	if err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(getUsers)

	handler.ServeHTTP(rr, req)

	// Verificar status
	if status := rr.Code; status != http.StatusOK {
		t.Errorf("handler retornou status errado: got %v want %v", status, http.StatusOK)
	}

	// Verificar content-type
	if ctype := rr.Header().Get("Content-Type"); ctype != "application/json" {
		t.Errorf("content-type errado: got %v want application/json", ctype)
	}

	// Verificar body
	var users []User
	if err := json.Unmarshal(rr.Body.Bytes(), &users); err != nil {
		t.Fatal(err)
	}

	if len(users) != 2 {
		t.Errorf("número de usuários errado: got %d want 2", len(users))
	}
}

func TestCreateUser(t *testing.T) {
	// Setup
	users = []User{} // Reset
	nextID = 1

	// Criar request
	newUser := User{Name: "Carol", Email: "carol@example.com"}
	body, _ := json.Marshal(newUser)
	
	req, err := http.NewRequest("POST", "/api/v1/users", bytes.NewBuffer(body))
	if err != nil {
		t.Fatal(err)
	}
	req.Header.Set("Content-Type", "application/json")

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(createUser)

	handler.ServeHTTP(rr, req)

	// Verificar
	if status := rr.Code; status != http.StatusCreated {
		t.Errorf("status errado: got %v want %v", status, http.StatusCreated)
	}

	var created User
	json.Unmarshal(rr.Body.Bytes(), &created)
	
	if created.Name != "Carol" {
		t.Errorf("nome errado: got %v want Carol", created.Name)
	}
}

func TestGetUserNotFound(t *testing.T) {
	req, err := http.NewRequest("GET", "/api/v1/users/999", nil)
	if err != nil {
		t.Fatal(err)
	}

	// Gorilla Mux precisa das vars
	vars := map[string]string{"id": "999"}
	req = mux.SetURLVars(req, vars)

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(getUser)

	handler.ServeHTTP(rr, req)

	if status := rr.Code; status != http.StatusNotFound {
		t.Errorf("status errado: got %v want %v", status, http.StatusNotFound)
	}
}

// Teste de integração
func TestAPIIntegration(t *testing.T) {
	// Setup router
	r := mux.NewRouter()
	api := r.PathPrefix("/api/v1").Subrouter()
	api.HandleFunc("/users", getUsers).Methods("GET")
	
	// Criar servidor de teste
	server := httptest.NewServer(r)
	defer server.Close()

	// Fazer request real
	resp, err := http.Get(server.URL + "/api/v1/users")
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Errorf("status errado: %d", resp.StatusCode)
	}
}

Deploy

Dockerfile

# Build stage
FROM golang:1.21-alpine AS builder

WORKDIR /app

# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download

# Copy source
COPY . .

# Build
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# Final stage
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy binary
COPY --from=builder /app/main .

# Copy config if needed
# COPY --from=builder /app/config.yaml .

EXPOSE 8080

CMD ["./main"]
# Build e run
docker build -t go-api .
docker run -p 8080:8080 go-api

Docker Compose

version: '3.8'

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/mydb?sslmode=disable
      - JWT_SECRET=supersecret
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

Deploy na Railway/Render/Fly.io

// main.go - Obter porta da variável de ambiente
package main

import (
	"log"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	router := setupRouter() // sua função de configuração

	log.Printf("Servidor iniciando na porta %s", port)
	log.Fatal(http.ListenAndServe(":"+port, router))
}

Documentação da API

Swagger/OpenAPI

// Instalação
// go get -u github.com/swaggo/http-swagger
// go get -u github.com/swaggo/swag/cmd/swag

// Documentação nos handlers
// @title API de Exemplo
// @version 1.0
// @description API REST em Go
// @host localhost:8080
// @BasePath /api/v1

// @Summary Listar usuários
// @Description Retorna todos os usuários cadastrados
// @Tags users
// @Accept json
// @Produce json
// @Success 200 {array} User
// @Router /users [get]
func getUsers(w http.ResponseWriter, r *http.Request) {
	// implementação
}

Checklist de Produção

Antes de deployar sua API:

  • Segurança: HTTPS, CORS configurado, headers de segurança
  • Autenticação: JWT ou OAuth2 implementado
  • Validação: Input validation em todos os endpoints
  • Logging: Structured logging (JSON)
  • Métricas: Prometheus/StatsD para monitoramento
  • Health checks: Endpoints /health e /ready
  • Graceful shutdown: Tratamento de sinais SIGTERM
  • Rate limiting: Proteção contra abuse
  • Database: Connection pooling, prepared statements
  • Timeouts: Configurados em todas as camadas
  • Testes: Cobertura > 80%
  • Documentação: Swagger/OpenAPI atualizado

Próximos Passos

Continue seu aprendizado:

  1. Golang para Iniciantes — Fundamentos da linguagem
  2. Go Concurrency Patterns — Goroutines e channels avançados
  3. Go Testing — Testes avançados e mocking
  4. Go e gRPC — APIs com Protocol Buffers

Go é excelente para APIs REST. Compartilhe o que você construiu!