Há pouco mais de dois anos, eu estava começando a usar Go de forma profissional, e fiquei muito entusiasmado em poder usar a biblioteca padrão net/http, para resolver problemas reais, sem a necessidade de incluir um montão de libs como dependência. Essa acabou sendo uma política que também aplico quando desenvolvo com auxílio de LLMs.
E publiquei um texto quando as novidades estavam chegando, com a versão 1.22, da qual tínhamos acesso apenas à versão 1.22.rc.1:
Coisas que deixei passar
O código do artigo é esse:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /path/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "got path\n")
})
mux.HandleFunc("GET /gospel/{version}/{testament}/{book}/{chapter}/", func(w http.ResponseWriter, r *http.Request) {
version := r.PathValue("version")
testament := r.PathValue("testament")
book := r.PathValue("book")
chapter := r.PathValue("chapter")
fmt.Fprintf(w, "handling gospel with version=%v, testament=%v, book=%v, chapter=%v\n", version, testament, book, chapter)
})
http.ListenAndServe("127.0.0.1:8090", mux)
}
Vamos então a alguns testes.
$ go build main.go
$ ./main &
$ curl http://localhost:8090
404 page not found
$ curl http://localhost:8090/path
<a href="/path/">Temporary Redirect</a>.
$ curl http://localhost:8090/path/
got path
$ curl -L http://localhost:8090/path
got path
$ curl http://localhost:8090/path/uma-string-aleatoria
got path
$ curl http://localhost:8090/path/uma/string/aleatoria
got path
$ curl http://localhost:8090/gospel/nvi/new/lucas/10
<a href="/gospel/nvi/new/lucas/10/">Temporary Redirect</a>.
$ curl http://localhost:8090/gospel/nvi/new/lucas/10/
handling gospel with version=nvi, testament=new, book=lucas, chapter=10
$ curl -L http://localhost:8090/gospel/1/new/2/10/0101
handling gospel with version=1, testament=new, book=2, chapter=10
$ curl http://localhost:8090/gospel/nvi/new/lucas/10/coisas_aleatorioas
handling gospel with version=nvi, testament=new, book=lucas, chapter=10
$ fg
Send job 1 (./main &) to foreground
^C⏎
$ go version
go version go1.26.5 linux/amd64
O importante: nesse exemplo não tem a / final como opcional, mas tem o
redirecionamento, causando um retorno e uma nova requisição. Também não tem um
limitador forte de onde acaba a URL.
Reforço o “nesse exemplo”, pois existe uma forma de limitar estritamente o final da URL que aceitamos, é o pattern {$}.
Observe os redirecionamentos que o Go gera, confirmados pelo uso da flag -L
do curl.
Refatorando esse velho código para algo mais próximo da realidade, e tratando do redirecionamento e do match estrito, tem-se:
package main
import (
"fmt"
"log"
"net/http"
)
func pathHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "got path\n")
}
func gospelHandler(w http.ResponseWriter, r *http.Request) {
version := r.PathValue("version")
testament := r.PathValue("testament")
book := r.PathValue("book")
chapter := r.PathValue("chapter")
fmt.Fprintf(
w,
"handling gospel with version=%v, testament=%v, book=%v, chapter=%v\n",
version,
testament,
book,
chapter,
)
}
func strictHandle(m *http.ServeMux, p string, h http.HandlerFunc) {
m.HandleFunc(p, h)
m.HandleFunc(p+"/{$}", h)
}
func main() {
mux := http.NewServeMux()
strictHandle(mux, "GET /path", pathHandler)
strictHandle(mux, "GET /gospel/{version}/{testament}/{book}/{chapter}", gospelHandler)
log.Fatal(http.ListenAndServe("127.0.0.1:8090", mux))
}
Primeiro dividi os handlers em funções, forma que costumo usar em meus sistemas.
Outra importante modificação é a inclusão da função strictHandle, uma
solução simples que configura duas vezes o mesmo endpoint, com barra no final e com o
pattern {$} para explicitamente limitar como se chama; e o sem barra no
final. Assim evitamos uma nova requisição de redirect.
Repetindo as requisições anteriores, sem a necessidade de testar com curl -L:
$ go build main.go
$ ./main &
$ curl http://localhost:8090
404 page not found
$ curl http://localhost:8090/path
got path
$ curl http://localhost:8090/path/
got path
$ curl http://localhost:8090/path/uma-string-aleatoria
404 page not found
$ curl http://localhost:8090/path/uma/string/aleatoria
404 page not found
$ curl http://localhost:8090/gospel/nvi/new/lucas/10
handling gospel with version=nvi, testament=new, book=lucas, chapter=10
$ curl http://localhost:8090/gospel/nvi/new/lucas/10/
handling gospel with version=nvi, testament=new, book=lucas, chapter=10
$ curl http://localhost:8090/gospel/nvi/new/lucas/10/coisas_aleatorias
404 page not found
$ fg
./main
^C⏎
$ go version
go version go1.26.5 linux/amd64
Agora vem o que me obrigou a fazer um novo teste, e quando comecei percebi que deveria refatorar.
O novo método QUERY
Temos uma RFC; quanto tempo que eu não lia uma RFC!
É a RFC 10008 - The HTTP QUERY Method, cujo abstract diz:
This specification defines the QUERY method for HTTP. A QUERY requests that the request target process the enclosed content in a safe and idempotent manner and then respond with the result of that processing. This is similar to POST requests, but QUERY requests can be automatically repeated or restarted without concern for partial state changes.
Independente de qualquer argumentação mais técnica ou filosófica, sempre me desagradou fazer consultas com query_strings. Quando os parâmetros começam a crescer, o debug vira um saco: escapes por toda parte, e admitamos, uma porcaria para ler.
Basta olhar para um verbo QUERY para saber imediatamente que, apesar de existir um corpo na requisição, trata-se de uma consulta.
Como o método é seguro e idempotente, clientes e intermediários podem reenviar a requisição automaticamente em situações apropriadas, sem o risco de provocar efeitos colaterais no servidor.
Pra exemplificar meu drama, a query bonitinha em JSON, pode ser algo como:
{
"output": "csv",
"filters": {
"name": "xyz",
"year": 2026
},
"fields": [
"name",
"age",
"city",
"active"
]
}
Que “antigamente” poderia virar um monstrengo como:
?output=csv&filters%5Bname%5D=xyz&filters%5Byear%5D=2026&fields%5B%5D=name&fields%5B%5D=age&fields%5B%5D=city&fields%5B%5D=active
Minha tentativa de implementação em Go:
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func pathHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
body, _ := io.ReadAll(r.Body)
fmt.Printf("got path; body: [%s]\n", body)
}
func strictHandle(m *http.ServeMux, p string, h http.HandlerFunc) {
m.HandleFunc(p, h)
m.HandleFunc(p+"/{$}", h)
}
func main() {
mux := http.NewServeMux()
strictHandle(mux, "QUERY /path", pathHandler)
log.Fatal(http.ListenAndServe("127.0.0.1:8090", mux))
}
Alguns exemplos:
$ go build main.go
$ ./main&
$ curl -L -X QUERY http://localhost:8090/path -d '{"output":"csv","filters":{"name":"xyz","year":2026},"fields":["name","age","city","active"]}'
got path; body: [{"output":"csv","filters":{"name":"xyz","year":2026"},"fields":["name","age","city","active"]}]
$ curl -L -X QUERY http://localhost:8090/path/ -d '{"output":"csv","filters":{"name":"xyz","year":2026},"fields":["name","age","city","active"]}'
got path; body: [{"output":"csv","filters":{"name":"xyz","year":2026},"fields":["name","age","city","active"]}]
curl -L -X QUERY http://localhost:8090/path/
got path; body: []
Para não complicar, não trabalhei o JSON, pois o importante aqui é a ideia.
Funciona com NGINX?
O NGINX não está nem aí!
Temos um Dockerfile para teste:
FROM golang:alpine AS builder
WORKDIR /app
COPY main.go .
RUN go build -o server main.go
FROM nginx:alpine
COPY --from=builder /app/server /usr/local/bin/server
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY start.sh /start.sh
RUN chmod +x /start.sh
CMD ["/start.sh"]
Uma configuração mínima de proxy reverso para o Nginx: (nginx.conf):
server {
listen 80;
location / {
proxy_pass http://127.0.0.1:8090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
E um script de inicialização que não daemoniza o servidor:
#!/bin/sh
/usr/local/bin/server &
nginx -g 'daemon off;'
Para executar:
$ docker build -t query-test .
(muitas linhas)
$ docker run -d --name query-test -p 8080:80 query-test
b5a998272ea34ac832b880c18599d9a170c11ec1f75e0f3aefc723126ead0811
$ curl -L -X QUERY http://localhost:8080/path/
$ curl -L -X QUERY http://localhost:8080/path/ -d '{"output":"csv","filters":{"name":"xyz","year":2026},"fields":["name","age","city","active"]}'
$ curl -L -X QUERY http://localhost:8080/path -d '{"output":"csv","filters":{"name":"xyz","year":2026},"fields":["name","age","city","active"]}'
$ docker logs query-test
(muitas linhas)
got path; body: []
172.17.0.1 - - [11/Jul/2026:23:54:17 +0000] "QUERY /path/ HTTP/1.1" 200 0 "-" "curl/8.18.0" "-"
got path; body: [{"output":"csv","filters":{"name":"xyz","year":2026},"fields":["name","age","city","active"]}]
172.17.0.1 - - [11/Jul/2026:23:54:26 +0000] "QUERY /path/ HTTP/1.1" 200 0 "-" "curl/8.18.0" "-"
got path; body: [{"output":"csv","filters":{"name":"xyz","year":2026},"fields":["name","age","city","active"]}]
172.17.0.1 - - [11/Jul/2026:23:54:33 +0000] "QUERY /path HTTP/1.1" 200 0 "-" "curl/8.18.0" "-"
Como diriam alguns professores meus, CQD (Como Queríamos Demonstrar).
Claro, “na minha máquina é fácil”, falta um teste real por trás de muitos equipamentos, WAF, balanceadores, etc.
Ainda há um caminho até que o método esteja amplamente difundido, mas a primeira impressão foi surpreendentemente positiva.

