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:
- Golang para Iniciantes — Fundamentos da linguagem
- Go Concurrency Patterns — Goroutines e channels avançados
- Go Testing — Testes avançados e mocking
- Go e gRPC — APIs com Protocol Buffers
Go é excelente para APIs REST. Compartilhe o que você construiu!