Pular para o conteúdo

Go

Go foi feita pra HTTP. A stdlib net/http aceita qualquer string como método em http.NewRequest — não precisa de monkey-patch, flag experimental, nem dependência extra. QUERY funciona out-of-the-box desde Go 1.0. Com o roteador melhorado do Go 1.22+, o ServeMux aceita "QUERY /path" direto no pattern. Frameworks como Chi também suportam métodos customizados com uma linha de registro.


O handler mais idiomático possível: ServeMux do Go 1.22+ com pattern "QUERY /rota".

package main
import (
"encoding/json"
"log"
"net/http"
)
// SearchRequest representa o body de uma requisição QUERY.
type SearchRequest struct {
Filter map[string]string `json:"filter"`
Fields []string `json:"fields"`
Limit int `json:"limit,omitempty"`
}
// SearchResponse é o payload de resposta.
type SearchResponse struct {
Results []map[string]any `json:"results"`
Total int `json:"total"`
}
func queryHandler(w http.ResponseWriter, r *http.Request) {
var req SearchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest)
return
}
// Simula busca filtrada
results := []map[string]any{
{"id": 1, "name": "Teclado Mecânico", "status": "active"},
{"id": 2, "name": "Monitor 4K", "status": "active"},
}
resp := SearchResponse{Results: results, Total: len(results)}
w.Header().Set("Content-Type", "application/json")
// QUERY é cacheable — adicione headers de cache
w.Header().Set("Cache-Control", "public, max-age=60")
json.NewEncoder(w).Encode(resp)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("QUERY /products", queryHandler)
log.Println("servidor rodando em :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}

Pontos-chave:

  • "QUERY /products" no pattern — Go 1.22+ roteia pelo método automaticamente.
  • Sem framework, sem dependência. Só stdlib.
  • Cache-Control sinaliza que a resposta é cacheável (semântica QUERY).

O cliente Go já suporta "QUERY" como método — é só uma string. Nenhum hack.

package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
payload := map[string]any{
"filter": map[string]string{"status": "active", "category": "electronics"},
"fields": []string{"id", "name", "price"},
"limit": 10,
}
body, err := json.Marshal(payload)
if err != nil {
log.Fatal(err)
}
req, err := http.NewRequest("QUERY", "http://localhost:8080/products", bytes.NewReader(body))
if err != nil {
log.Fatal(err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %s\nBody: %s\n", resp.Status, data)
}

Por que funciona: http.NewRequest aceita qualquer string no primeiro parâmetro. O transport HTTP/1.1 do Go envia o método literal no request line: QUERY /products HTTP/1.1.


Chi precisa de chi.RegisterMethod no init() pra reconhecer métodos não-padrão no roteamento.

package main
import (
"encoding/json"
"log"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func init() {
// Registra QUERY como método reconhecido pelo roteador
chi.RegisterMethod("QUERY")
}
type SearchRequest struct {
Filter map[string]string `json:"filter"`
Fields []string `json:"fields"`
Limit int `json:"limit,omitempty"`
}
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// MethodFunc registra handler para método customizado
r.MethodFunc("QUERY", "/products", func(w http.ResponseWriter, r *http.Request) {
var req SearchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest)
return
}
results := map[string]any{
"results": []map[string]any{
{"id": 1, "name": "Teclado Mecânico", "price": 349.90},
},
"total": 1,
"query": req,
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "public, max-age=60")
json.NewEncoder(w).Encode(results)
})
log.Println("chi server em :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}

Detalhes:

  • chi.RegisterMethod("QUERY") no init() — obrigatório antes de criar o router.
  • r.MethodFunc("QUERY", "/products", handler) — registra a rota.
  • Todo middleware do Chi funciona normalmente (Logger, Recoverer, etc).

Teste de integração idiomático sem subir servidor real.

package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestQueryHandler(t *testing.T) {
// Monta o handler
mux := http.NewServeMux()
mux.HandleFunc("QUERY /products", queryHandler)
// Cria request QUERY com body JSON
payload := map[string]any{
"filter": map[string]string{"status": "active"},
"fields": []string{"id", "name"},
}
body, _ := json.Marshal(payload)
req := httptest.NewRequest("QUERY", "/products", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
// Grava a resposta
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
// Verifica status
if rec.Code != http.StatusOK {
t.Fatalf("esperava 200, recebeu %d", rec.Code)
}
// Verifica Content-Type
ct := rec.Header().Get("Content-Type")
if ct != "application/json" {
t.Fatalf("esperava application/json, recebeu %s", ct)
}
// Verifica Cache-Control (QUERY é cacheable)
cc := rec.Header().Get("Cache-Control")
if cc == "" {
t.Fatal("esperava Cache-Control header pra método QUERY")
}
// Decodifica resposta
var resp SearchResponse
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
t.Fatalf("erro decodificando resposta: %v", err)
}
if resp.Total == 0 {
t.Fatal("esperava resultados, recebeu zero")
}
}
func TestQueryHandler_InvalidJSON(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("QUERY /products", queryHandler)
req := httptest.NewRequest("QUERY", "/products", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("esperava 400 pra JSON inválido, recebeu %d", rec.Code)
}
}
func TestQueryHandler_WrongMethod(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("QUERY /products", queryHandler)
// GET não deve atingir o handler QUERY
req := httptest.NewRequest("GET", "/products", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code == http.StatusOK {
t.Fatal("GET não deveria retornar 200 em rota QUERY")
}
}

httptest.NewRequest aceita qualquer método — mesmo princípio do http.NewRequest. Sem mágica.


curl suporta qualquer método via -X. Aqui os cenários mais úteis:

Terminal window
# Requisição QUERY básica com body JSON
curl -X QUERY http://localhost:8080/products \
-H "Content-Type: application/json" \
-d '{"filter":{"status":"active"},"fields":["id","name"],"limit":10}'
curl -X QUERY http://localhost:8080/products \
-H "Content-Type: application/json" \
-d '{"filter":{"category":"electronics"}}' \
-v
echo '{"filter":{"status":"active","price_min":100},"fields":["id","name","price"]}' > query.json
curl -X QUERY http://localhost:8080/products \
-H "Content-Type: application/json" \
-d @query.json
curl -X QUERY http://localhost:8080/products \
-H "Content-Type: application/json" \
-d '{"filter":{"status":"active"}}' \
-s -o /dev/null -D -

Saída esperada do verbose:

> QUERY /products HTTP/1.1
> Host: localhost:8080
> Content-Type: application/json
> Content-Length: 65
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Cache-Control: public, max-age=60

Aspecto Como Go resolve
Método customizado http.NewRequest("QUERY", ...) — é só string
Roteamento ServeMux Go 1.22+ aceita "QUERY /path"
Body em safe method Transport envia body normalmente
Cache semântico Headers padrão, sem hack
Teste httptest.NewRequest("QUERY", ...) direto
Frameworks Chi: RegisterMethod + MethodFunc

Sem reflection, sem build tag, sem flag. O design do net/http já contemplava métodos arbitrários desde o dia zero.