Roteamento e Middlewares sem Framework em Golang

1. Fundamentos do Servidor HTTP Padrão

O pacote net/http da biblioteca padrão do Go fornece tudo que precisamos para construir servidores web robustos sem frameworks externos. A interface central é o http.Handler:

type Handler interface {
    ServeHTTP(http.ResponseWriter, *http.Request)
}

Para iniciar um servidor básico:

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Olá, mundo!"))
    })
    http.ListenAndServe(":8080", nil)
}

O multiplexador padrão (http.DefaultServeMux) é limitado — não suporta parâmetros de rota dinâmicos nem métodos HTTP específicos. Para superar essas limitações, construímos nosso próprio roteador.

2. Construindo um Roteador Manual

Criamos uma estrutura que mapeia padrões de URL a handlers, armazenando também o método HTTP esperado:

type route struct {
    method  string
    pattern string
    handler http.HandlerFunc
}

type Router struct {
    routes []route
}

func (r *Router) Handle(method, pattern string, handler http.HandlerFunc) {
    r.routes = append(r.routes, route{method, pattern, handler})
}

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for _, route := range r.routes {
        if route.method == req.Method && route.pattern == req.URL.Path {
            route.handler(w, req)
            return
        }
    }
    http.NotFound(w, req)
}

Para extrair parâmetros de query string, usamos r.URL.Query():

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for _, route := range r.routes {
        if route.method == req.Method && route.pattern == req.URL.Path {
            // Parâmetros de query
            id := req.URL.Query().Get("id")
            if id != "" {
                req = req.WithContext(context.WithValue(req.Context(), "id", id))
            }
            route.handler(w, req)
            return
        }
    }
    http.NotFound(w, req)
}

3. Implementação de Middlewares

Um middleware é uma função que recebe um http.Handler e retorna outro http.Handler. Essa assinatura permite encadeamento:

type Middleware func(http.Handler) http.Handler

Exemplo de 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 recuperação de panics:

func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Composição de middlewares:

func ApplyMiddleware(handler http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

4. Roteamento com Métodos HTTP

Evoluímos nosso roteador para tratar métodos HTTP explicitamente:

type Router struct {
    routes map[string]map[string]http.HandlerFunc
}

func NewRouter() *Router {
    return &Router{
        routes: make(map[string]map[string]http.HandlerFunc),
    }
}

func (r *Router) Handle(method, pattern string, handler http.HandlerFunc) {
    if r.routes[pattern] == nil {
        r.routes[pattern] = make(map[string]http.HandlerFunc)
    }
    r.routes[pattern][method] = handler
}

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    if handlers, ok := r.routes[req.URL.Path]; ok {
        if handler, ok := handlers[req.Method]; ok {
            handler(w, req)
            return
        }
        http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        return
    }
    http.NotFound(w, req)
}

// Métodos auxiliares
func (r *Router) GET(pattern string, handler http.HandlerFunc)  { r.Handle("GET", pattern, handler) }
func (r *Router) POST(pattern string, handler http.HandlerFunc) { r.Handle("POST", pattern, handler) }

5. Parâmetros de Rota e Validação

Suporte a parâmetros dinâmicos como /users/:id:

type Router struct {
    routes     []route
    middleware []Middleware
}

type route struct {
    method  string
    pattern string
    handler http.HandlerFunc
    params  []string
}

func (r *Router) addRoute(method, pattern string, handler http.HandlerFunc) {
    params := extractParams(pattern)
    r.routes = append(r.routes, route{
        method:  method,
        pattern: pattern,
        handler: handler,
        params:  params,
    })
}

func extractParams(pattern string) []string {
    var params []string
    segments := strings.Split(pattern, "/")
    for _, seg := range segments {
        if strings.HasPrefix(seg, ":") {
            params = append(params, seg[1:])
        }
    }
    return params
}

func matchPath(pattern, path string) (map[string]string, bool) {
    patternSegs := strings.Split(pattern, "/")
    pathSegs := strings.Split(path, "/")
    if len(patternSegs) != len(pathSegs) {
        return nil, false
    }
    params := make(map[string]string)
    for i, seg := range patternSegs {
        if strings.HasPrefix(seg, ":") {
            params[seg[1:]] = pathSegs[i]
        } else if seg != pathSegs[i] {
            return nil, false
        }
    }
    return params, true
}

Validação de parâmetros:

func validateID(id string) (int, error) {
    n, err := strconv.Atoi(id)
    if err != nil || n <= 0 {
        return 0, fmt.Errorf("invalid ID: %s", id)
    }
    return n, nil
}

6. Agrupamento de Rotas e Sub-roteadores

Criamos sub-roteadores com prefixo comum:

type SubRouter struct {
    prefix string
    router *Router
}

func (r *Router) Group(prefix string) *SubRouter {
    return &SubRouter{
        prefix: prefix,
        router: r,
    }
}

func (sr *SubRouter) Handle(method, pattern string, handler http.HandlerFunc) {
    sr.router.Handle(method, sr.prefix+pattern, handler)
}

func (sr *SubRouter) Use(mw Middleware) {
    sr.router.Use(mw)
}

Uso prático:

router := NewRouter()
api := router.Group("/api/v1")
api.Use(AuthMiddleware)
api.GET("/users", listUsers)
api.POST("/users", createUser)

7. Tratamento de Erros e Respostas Consistentes

Helpers para respostas JSON padronizadas:

type APIResponse struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data,omitempty"`
    Error   string      `json:"error,omitempty"`
}

func JSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{
        Success: status < 400,
        Data:    data,
    })
}

func ErrorJSON(w http.ResponseWriter, status int, message string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{
        Success: false,
        Error:   message,
    })
}

Middleware global de tratamento de erros:

func ErrorHandlerMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                ErrorJSON(w, http.StatusInternalServerError, "internal server error")
            }
        }()
        next.ServeHTTP(w, r)
    })
}

8. Testes do Roteador e Middlewares

Testes unitários com httptest:

func TestRouter_GET(t *testing.T) {
    router := NewRouter()
    router.GET("/hello", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("world"))
    })

    req := httptest.NewRequest("GET", "/hello", nil)
    rec := httptest.NewRecorder()
    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.Equal(t, "world", rec.Body.String())
}

func TestRouter_MethodNotAllowed(t *testing.T) {
    router := NewRouter()
    router.GET("/resource", handler)

    req := httptest.NewRequest("POST", "/resource", nil)
    rec := httptest.NewRecorder()
    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
}

func TestMiddlewareChain(t *testing.T) {
    var log []string
    mw1 := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log = append(log, "mw1")
            next.ServeHTTP(w, r)
        })
    }
    mw2 := func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            log = append(log, "mw2")
            next.ServeHTTP(w, r)
        })
    }

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log = append(log, "handler")
    })

    final := ApplyMiddleware(handler, mw1, mw2)
    final.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))

    assert.Equal(t, []string{"mw1", "mw2", "handler"}, log)
}

func TestRouter_Params(t *testing.T) {
    router := NewRouter()
    router.GET("/users/:id", func(w http.ResponseWriter, r *http.Request) {
        params := r.Context().Value("params").(map[string]string)
        w.Write([]byte(params["id"]))
    })

    req := httptest.NewRequest("GET", "/users/42", nil)
    rec := httptest.NewRecorder()
    router.ServeHTTP(rec, req)

    assert.Equal(t, "42", rec.Body.String())
}

Conclusão

Construir roteamento e middlewares sem frameworks em Go não apenas é possível, como também oferece controle total sobre o comportamento do servidor. A abordagem apresentada demonstra como:

  • O pacote net/http fornece todos os blocos de construção necessários
  • Middlewares como funções de alta ordem permitem composição elegante
  • O roteador manual pode ser estendido para suportar parâmetros, validação e sub-roteadores
  • Testes com httptest garantem a confiabilidade do sistema

Para aplicações que exigem simplicidade e desempenho, essa abordagem supera muitos frameworks, mantendo o código enxuto e compreensível.

Referências