A biblioteca padrão do Go traz um servidor HTTP pronto para produção. Aprenda handlers, roteamento com padrões de método e caminho no ServeMux, as regras do ResponseWriter, o que acontece com cada requisição e por que o http.Server precisa de timeouts.
Go não precisa de um framework web para servir HTTP. O pacote net/http da biblioteca padrão tem um servidor de verdade, um roteador e um cliente, e grandes serviços em produção rodam direto sobre ele.
Este post cobre handlers, roteamento com http.ServeMux, as regras para escrever uma resposta, o que acontece com uma requisição do começo ao fim, e por que você deve montar um http.Server por conta própria em vez de chamar http.ListenAndServe. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.
Um handler é um método
Um handler em Go é qualquer valor com um método ServeHTTP. O pacote net/http o define como uma interface:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
É a mesma ideia da parte sobre interfaces: uma interface pequena, satisfeita sem precisar declarar isso. O servidor chama ServeHTTP uma vez para cada requisição. O *http.Request guarda o que o cliente enviou. O http.ResponseWriter é onde você escreve a resposta.
Aqui está um handler feito a partir de uma struct. Para rodar um servidor de verdade dentro de um exemplo verificado, usamos httptest.NewServer. Ele sobe o seu handler numa porta livre aleatória da sua própria máquina, entrega o endereço em srv.URL e o desliga quando você chama Close. O lado do cliente é http.Get, que envia uma requisição real por uma conexão real.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
type greeter struct {
greeting string
}
func (g greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, you asked for %s\n", g.greeting, r.URL.Path)
}
func main() {
var h http.Handler = greeter{greeting: "Hello"}
srv := httptest.NewServer(h)
defer srv.Close()
res, err := http.Get(srv.URL + "/tasks")
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(res.Status)
fmt.Println(res.Header.Get("Content-Type"))
fmt.Print(string(body))
}
Ele imprime:
200 OK
text/plain; charset=utf-8
Hello, you asked for /tasks
greeter nunca diz que implementa http.Handler. Ele tem o método, então a atribuição compila. fmt.Fprintf funciona com w porque um ResponseWriter também é um io.Writer, a interface que você conheceu junto com io.Reader e io.Writer.
O handler nunca definiu um status nem um Content-Type, e o cliente recebeu os dois mesmo assim. Quando você não escolhe, o servidor envia 200 e adivinha o tipo de conteúdo pelos primeiros bytes que você escreve. Mais abaixo você vai ver exatamente quando isso acontece.
http.HandlerFunc transforma uma função em handler
A maioria dos handlers não precisa de uma struct, então o net/http tem um adaptador. http.HandlerFunc é um tipo de função com um método ServeHTTP que simplesmente chama a função:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
Converter uma função comum para esse tipo dá a ela o método, e ela vira um http.Handler. Este programa também usa a segunda ferramenta do httptest. httptest.NewRecorder é um ResponseWriter que guarda o que o handler escreveu, então você pode chamar um handler direto, sem rede nenhuma:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func health(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
}
func main() {
h := http.HandlerFunc(health)
fmt.Printf("%T\n", h)
req := httptest.NewRequest("GET", "/health", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Println(rec.Code)
fmt.Print(rec.Body.String())
}
Ele imprime:
http.HandlerFunc
200
ok
http.HandlerFunc(health) é uma conversão de tipo, não uma chamada. Nada roda até alguém chamar ServeHTTP. httptest.NewServer testa o percurso inteiro por uma conexão. httptest.NewRecorder testa só o handler, o que é mais rápido. A parte sobre testar e colocar a API em produção vai muito mais fundo nos dois.
Roteamento com http.ServeMux
Um servidor de verdade tem mais de um handler, então alguma coisa precisa escolher qual deles recebe cada requisição. No net/http, essa coisa é o http.ServeMux, um roteador que também é um handler. Você registra padrões nele, e o ServeHTTP dele escolhe o handler certo e o chama.
Desde o Go 1.22, um padrão pode indicar um método HTTP além de um caminho, e um caminho pode ter wildcards entre chaves. Dentro do handler, r.PathValue retorna o que um wildcard capturou:
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "task %s\n", r.PathValue("id"))
})
mux.HandleFunc("DELETE /tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "deleted task %s\n", r.PathValue("id"))
})
srv := httptest.NewServer(mux)
defer srv.Close()
send := func(method, path string) {
req, _ := http.NewRequest(method, srv.URL+path, nil)
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Printf("%-6s %-12s %d %q", method, path, res.StatusCode, body)
if allow := res.Header.Get("Allow"); allow != "" {
fmt.Printf(" Allow: %s", allow)
}
fmt.Println()
}
send("GET", "/tasks/42")
send("DELETE", "/tasks/7")
send("PUT", "/tasks/42")
send("GET", "/tasks")
send("GET", "/tasks/a%2Fb")
send("GET", "/users/1")
}
Ele imprime:
GET /tasks/42 200 "task 42\n"
DELETE /tasks/7 200 "deleted task 7\n"
PUT /tasks/42 405 "Method Not Allowed\n" Allow: DELETE, GET, HEAD
GET /tasks 404 "404 page not found\n"
GET /tasks/a%2Fb 200 "task a/b\n"
GET /users/1 404 "404 page not found\n"
Leia as linhas uma de cada vez:
GET /tasks/42eDELETE /tasks/7chegam cada um ao seu handler, e{id}capturou o número.PUT /tasks/42encontrou um caminho que existe, mas nenhum padrão aceitaPUT. O ServeMux respondeu 405 Method Not Allowed sozinho, com um cabeçalhoAllowlistando os métodos que funcionariam.HEADestá na lista porque um padrãoGETtambém casa com requisiçõesHEAD.GET /tasksé um 404, não um 405.{id}precisa casar com um segmento do caminho, e não há nenhum, então nenhum padrão casa com o caminho./tasks/a%2Fbnos surpreendeu.%2Fé uma barra escapada, então o wildcard vê um único segmento. MasPathValueo retorna sem o escape, comoa/b. Trate um valor de caminho como qualquer outra entrada vinda de um cliente, e confira antes de usar.- Um caminho que nenhum padrão conhece recebe 404 com o corpo
404 page not found.
O método num padrão vem antes do caminho, separado por um espaço, e um padrão sem método casa com todos os métodos. Antes do Go 1.22 nada disso existia. As pessoas conferiam r.Method na mão ou recorriam a um roteador de terceiros. Para a maioria das APIs, hoje você não precisa de um.
Explicado como se você tivesse dez anos
Imagine a sala de triagem de uma agência dos correios. Uma parede de escaninhos tem um endereço escrito em cima de cada um: “Rua Principal, 12”, “Rua Principal, qualquer casa”, “Qualquer lugar da cidade”. Chega uma carta, e o triador lê o endereço e a coloca num escaninho. Um carteiro pega tudo o que está naquele escaninho e entrega.
Se um endereço serve para mais de um escaninho, o triador escolhe o mais exato. Uma carta para a Rua Principal, 12 vai no escaninho “Rua Principal, 12”, mesmo que “Rua Principal, qualquer casa” também a aceitasse. Se nenhum escaninho serve, a carta volta com o carimbo “endereço desconhecido”.
A versão precisa
O ServeMux guarda um conjunto de padrões, cada um ligado a um handler. Para cada requisição, ele encontra todos os padrões que casam com o método e o caminho. Se vários casarem, o mais específico vence. Um padrão é mais específico que outro se casa com um subconjunto estrito das requisições com que o outro casa. A ordem em que você os registrou não importa.
Se nada casa com o caminho, o ServeMux chama o seu handler embutido de “não encontrado”, que escreve 404. Se o caminho casa mas o método não, ele escreve 405 com um cabeçalho Allow.
Onde a analogia falha: uma agência dos correios separa só pelo endereço. O ServeMux também lê o método, o que é como separar pelo endereço e pelo que está escrito no envelope: “entregar”, “recolher” ou “cancelar”. Um wildcard também faz mais que um escaninho: ele copia parte do endereço, como 42, e a entrega ao carteiro. E uma agência dos correios dá um jeito quando dois escaninhos são igualmente exatos. O ServeMux se recusa a iniciar, como mostra a próxima seção.
Wildcards: {name...} e {$}
Um wildcard simples como {id} casa com exatamente um segmento do caminho. Duas formas especiais cobrem os outros casos. {path...} no fim de um padrão casa com todos os segmentos restantes, barras incluídas. {$} casa só com o fim do caminho, e é assim que você registra a raiz sem capturar tudo:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "home page")
})
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "file %q\n", r.PathValue("path"))
})
for _, path := range []string{"/", "/about", "/files/notes/2026/todo.txt", "/files/"} {
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
fmt.Printf("%-27s %d %s", path, rec.Code, rec.Body.String())
}
}
Ele imprime:
/ 200 home page
/about 404 404 page not found
/files/notes/2026/todo.txt 200 file "notes/2026/todo.txt"
/files/ 200 file ""
Sem {$}, o padrão GET / terminaria em barra, e um padrão que termina em barra casa com todo caminho abaixo dele. /about receberia a página inicial em vez de um 404. O wildcard {path...} também pode casar com nada, e é por isso que /files/ chegou ao handler com uma string vazia.
O padrão mais específico vence
Quando uma requisição casa com vários padrões, o ServeMux não pega o primeiro registrado. Ele pega o que casa com o menor número possível de requisições. Este programa registra quatro padrões sobrepostos, de propósito na ordem “errada”:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func reply(text string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, text)
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/tasks/{id}", reply(`"/tasks/{id}"`))
mux.HandleFunc("GET /tasks/{id}", reply(`"GET /tasks/{id}"`))
mux.HandleFunc("GET /tasks/new", reply(`"GET /tasks/new"`))
mux.HandleFunc("/", reply(`"/"`))
for _, t := range []struct{ method, path string }{
{"GET", "/tasks/new"},
{"GET", "/tasks/42"},
{"PUT", "/tasks/42"},
{"GET", "/tasks/42/notes"},
} {
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(t.method, t.path, nil))
fmt.Printf("%-4s %-16s -> %s", t.method, t.path, rec.Body.String())
}
}
Ele imprime:
GET /tasks/new -> "GET /tasks/new"
GET /tasks/42 -> "GET /tasks/{id}"
PUT /tasks/42 -> "/tasks/{id}"
GET /tasks/42/notes -> "/"
Um segmento literal, new, vence um wildcard, {id}. Um padrão com método vence o mesmo caminho sem método, então GET vai para o handler específico do método e PUT cai no que aceita qualquer método. / termina em barra, então casa com todo caminho, e /tasks/42/notes cai ali porque nada mais serve. É também por isso que não há 405 neste programa: o padrão pega-tudo sempre casa.
Às vezes nenhum dos dois padrões é mais específico. GET /{kind}/42 e /tasks/{id} casam com /tasks/42, mas cada um também casa com caminhos com que o outro não casa. Registrar os dois faz o HandleFunc entrar em panic na inicialização. A mensagem cita os dois padrões e a linha em que cada um foi registrado, e depois explica:
GET /{kind}/42 and /tasks/{id} both match some paths, like "/tasks/42".
But neither is more specific than the other.
Um panic na inicialização é do tipo que ajuda. Você descobre a ambiguidade na primeira vez que roda o servidor, e não quando chega uma requisição azarada.
Escrevendo uma resposta: cabeçalho, status, corpo
Uma resposta HTTP sai numa ordem fixa: a linha de status, depois os cabeçalhos, depois o corpo. O ResponseWriter obriga você a seguir essa ordem, porque, depois que uma parte já saiu, ela não pode mais mudar. Três chamadas correspondem às três partes:
w.Header()retorna o map de cabeçalhos. Mude-o primeiro.w.WriteHeader(code)envia a linha de status e os cabeçalhos.w.Writeenvia bytes do corpo. SeWriteHeaderainda não foi chamado, o primeiroWritechamaWriteHeader(200)por você.
Aqui está um handler que faz isso na ordem certa:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func create(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Location", "/tasks/43")
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "created task 43")
}
func main() {
rec := httptest.NewRecorder()
create(rec, httptest.NewRequest("POST", "/tasks", nil))
res := rec.Result()
fmt.Println(res.Status)
fmt.Println("Location:", res.Header.Get("Location"))
fmt.Print(rec.Body.String())
}
Ele imprime:
201 Created
Location: /tasks/43
created task 43
Use rec.Result() para ler o que um cliente veria. rec.Header() é o map vivo do handler, e ainda mostra cabeçalhos que foram definidos tarde demais para serem enviados.
Agora a ordem errada. Este handler escreve o corpo primeiro e depois tenta definir um cabeçalho e um 404. Para ver a reclamação do servidor, o programa monta o servidor de teste com httptest.NewUnstartedServer, aponta o ErrorLog dele para o stdout sem timestamp, e só então o inicia:
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
)
func lateHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "task 42")
w.Header().Set("X-Task-Id", "42")
w.WriteHeader(http.StatusNotFound)
}
func main() {
srv := httptest.NewUnstartedServer(http.HandlerFunc(lateHandler))
// Send the server's error log to stdout, with no timestamp, so we can see it.
srv.Config.ErrorLog = log.New(os.Stdout, "server log: ", 0)
srv.Start()
defer srv.Close()
res, err := http.Get(srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println("status:", res.StatusCode)
fmt.Printf("X-Task-Id: %q\n", res.Header.Get("X-Task-Id"))
fmt.Print("body: ", string(body))
}
Ele imprime:
server log: http: superfluous response.WriteHeader call from main.lateHandler (main.go:15)
status: 200
X-Task-Id: ""
body: task 42
O cliente recebeu um 200, não um 404, e nenhum cabeçalho X-Task-Id. O primeiro Fprintln fixou o status em 200 e congelou os cabeçalhos. O Header().Set atrasado mudou um map que ninguém lê mais, e fez isso em silêncio. O WriteHeader atrasado não fez nada, a não ser deixar uma linha “superfluous” no log do servidor, com a função e o número da linha. Se você vir essa linha num log de verdade, procure um caminho de erro que escreve depois que o corpo já começou.
A vida de uma requisição
Uma requisição passa por várias mãos entre o cliente e o seu handler, e cada uma faz um único trabalho. Acompanhe uma requisição do começo ao fim:
Uma requisição do começo ao fim. O servidor dá à conexão uma goroutine própria, o ServeMux escolhe o padrão mais específico que casa, e o handler lê o valor do caminho e depois escreve o cabeçalho, o status e o corpo, nessa ordem, antes de a resposta voltar.
Aqui estão esses passos em palavras, caso a animação não rode para você:
- Um cliente envia
GET /tasks/42. - O servidor aceita a conexão e inicia uma nova goroutine para tratá-la.
- O ServeMux compara a requisição com seus padrões,
GET /tasks,GET /tasks/{id}ePOST /tasks, e escolheGET /tasks/{id}. - O handler desse padrão roda e chama
r.PathValue("id"), que retorna"42". - O handler define um cabeçalho, chama
WriteHeader(200)e escreve o corpo. - A resposta volta para o cliente, e a goroutine não tem mais nada a fazer.
O passo 2 importa mais do que parece. O servidor roda Accept num loop e, para cada conexão nova, inicia uma goroutine, como na parte sobre goroutines. As requisições que chegam por essa conexão se revezam na goroutine dela, e no HTTP/2 cada requisição ganha uma goroutine só sua. De um jeito ou de outro, cem clientes significam cem goroutines rodando seus handlers ao mesmo tempo, e você nunca escreveu go.
Este programa prova que dois handlers realmente rodam ao mesmo tempo. /wait fica bloqueado até um channel ser fechado, e só /release o fecha:
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
func get(url string) string {
res, err := http.Get(url)
if err != nil {
return "error: " + err.Error()
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
return string(body)
}
func main() {
release := make(chan struct{})
mux := http.NewServeMux()
mux.HandleFunc("GET /wait", func(w http.ResponseWriter, r *http.Request) {
<-release // blocks until another request closes the channel
fmt.Fprint(w, "wait: released")
})
mux.HandleFunc("GET /release", func(w http.ResponseWriter, r *http.Request) {
close(release)
fmt.Fprint(w, "release: done")
})
srv := httptest.NewServer(mux)
defer srv.Close()
waitResult := make(chan string)
go func() { waitResult <- get(srv.URL + "/wait") }()
fmt.Println(get(srv.URL + "/release"))
fmt.Println(<-waitResult)
}
Ele imprime:
release: done
wait: released
Se o servidor tratasse uma requisição por vez, /wait prenderia o único worker para sempre, /release nunca rodaria e o programa travaria. Ele termina, então os dois handlers estavam rodando ao mesmo tempo.
Estado compartilhado num handler precisa de lock
Handlers que rodam ao mesmo tempo e mexem na mesma variável têm um data race, exatamente como as goroutines da parte sobre sync. Aqui o race passa despercebido com facilidade, porque nada no seu código inicia uma goroutine. Este handler conta visitas sem lock, e 50 requisições chegam juntas:
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync"
)
type visits struct {
count int
}
func (v *visits) ServeHTTP(w http.ResponseWriter, r *http.Request) {
v.count++ // no lock: every request runs in its own goroutine
fmt.Fprint(w, v.count)
}
func main() {
srv := httptest.NewServer(&visits{})
defer srv.Close()
var wg sync.WaitGroup
for range 50 {
wg.Go(func() {
res, err := http.Get(srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
io.Copy(io.Discard, res.Body)
res.Body.Close()
})
}
wg.Wait()
fmt.Println("done")
}
Rodando com go run -race ., ele imprime um relatório como este (as linhas ... representam endereços, caminhos de arquivo e números de goroutine que mudam a cada execução):
WARNING: DATA RACE
...
main.(*visits).ServeHTTP()
...
net/http.(*conn).serve()
...
done
exit status 66
Olhe a stack no relatório. Abaixo do seu ServeHTTP está net/http.(*conn).serve(), a goroutine que o servidor iniciou para cada conexão. O race detector encontrou duas delas gravando count sem nada que as separasse.
A correção é a da parte sobre sync: um mutex na struct, e um receiver de ponteiro para que todas as requisições compartilhem o mesmo mutex:
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync"
)
type visits struct {
mu sync.Mutex
count int
}
func (v *visits) ServeHTTP(w http.ResponseWriter, r *http.Request) {
v.mu.Lock()
v.count++
n := v.count
v.mu.Unlock()
fmt.Fprint(w, n)
}
func main() {
v := &visits{}
srv := httptest.NewServer(v)
defer srv.Close()
var wg sync.WaitGroup
for range 50 {
wg.Go(func() {
res, err := http.Get(srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
io.Copy(io.Discard, res.Body)
res.Body.Close()
})
}
wg.Wait()
v.mu.Lock()
fmt.Println("visits:", v.count)
v.mu.Unlock()
}
Ele imprime:
visits: 50
O handler copia a contagem para n enquanto segura o lock, e destrava antes de escrever a resposta. Escrever para um cliente lento pode demorar muito, e ninguém mais deveria esperar por isso. Tudo o que um handler lê ou escreve fora da própria requisição, como um map de tarefas, um cache ou um contador, precisa do mesmo cuidado.
r.Context() termina quando o cliente vai embora
Toda requisição carrega um context, e o servidor o cancela quando o cliente desconecta. É o context da parte sobre padrões de concorrência, já ligado para você. Um handler que faz trabalho lento deve observar r.Context().Done() e parar, porque ninguém está mais esperando a resposta.
Neste programa, o cliente desiste assim que o handler começa. O trabalho do handler levaria 10 segundos, uma margem folgada, então o resultado é sempre o mesmo:
package main
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"time"
)
func main() {
started := make(chan struct{})
outcome := make(chan string)
slow := func(w http.ResponseWriter, r *http.Request) {
close(started)
select {
case <-time.After(10 * time.Second):
fmt.Fprintln(w, "report ready")
outcome <- "handler: finished the work"
case <-r.Context().Done():
outcome <- "handler: stopped early: " + r.Context().Err().Error()
}
}
srv := httptest.NewServer(http.HandlerFunc(slow))
defer srv.Close()
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-started
cancel() // the client gives up once the handler is running
}()
req, _ := http.NewRequestWithContext(ctx, "GET", srv.URL, nil)
_, err := http.DefaultClient.Do(req)
fmt.Println("client: canceled:", errors.Is(err, context.Canceled))
fmt.Println(<-outcome)
}
Ele imprime:
client: canceled: true
handler: stopped early: context canceled
Cancelar o context do cliente fechou a conexão. O servidor percebeu, cancelou r.Context(), e o select do handler pegou o caso Done na hora, em vez de esperar 10 segundos. Num handler de verdade, você passa r.Context() para a consulta ao banco de dados ou para a chamada externa, e elas também param. O context também termina quando ServeHTTP retorna, então não o guarde para trabalho que deveria durar mais que a requisição.
Monte um http.Server, não chame http.ListenAndServe
O primeiro servidor Go de todo tutorial é http.ListenAndServe(":8080", mux). Funciona, mas monta um http.Server com todos os campos no zero value, e, para os timeouts, zero significa “esperar para sempre”.
Isso é um problema real na internet. Um cliente pode abrir uma conexão e enviar os cabeçalhos da requisição um byte a cada poucos segundos. Sem ReadHeaderTimeout, o servidor espera com paciência, segurando uma goroutine e uma conexão aberta. Com clientes assim em número suficiente, o servidor esgota as conexões abertas que consegue manter, sem nunca ver uma requisição completa. O ataque é antigo o bastante para ter nome, Slowloris, e custa quase nada ao atacante.
A correção é montar o servidor você mesmo e definir os timeouts:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "task %s\n", r.PathValue("id"))
})
srv := &http.Server{
Addr: "localhost:8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Println("listening on", srv.Addr)
log.Fatal(srv.ListenAndServe())
}
Este não roda no nosso verificador, porque escuta numa porta fixa até você pará-lo. Rode você mesmo e, de um segundo terminal:
curl localhost:8080/tasks/42
Isso imprime task 42. Veja o que cada timeout limita:
ReadHeaderTimeout: quanto tempo um cliente tem para enviar os cabeçalhos da requisição. É este que barra o Slowloris, e é o que você nunca deve deixar de fora.ReadTimeout: quanto tempo a leitura da requisição inteira pode levar, corpo incluído.WriteTimeout: quanto tempo o servidor tem para escrever a resposta depois que os cabeçalhos foram lidos.IdleTimeout: quanto tempo uma conexão keep-alive pode ficar parada entre requisições.
Os números acima são pontos de partida sensatos, não regras. Um servidor que aceita uploads grandes ou transmite respostas longas precisa de outros. ListenAndServe também bloqueia até o servidor falhar, e aí log.Fatal encerra o programa sem deixar as requisições em andamento terminarem. Escolher timeouts para produção e desligar o servidor de forma graciosa são assuntos da parte sobre testar e colocar a API em produção.
O que lembrar
- Um
http.Handleré qualquer tipo comServeHTTP(http.ResponseWriter, *http.Request).http.HandlerFunctransforma uma função comum em um. - Desde o Go 1.22, os padrões do ServeMux aceitam um método e wildcards:
"GET /tasks/{id}", lido comr.PathValue("id").{rest...}casa com o resto do caminho, e{$}casa só com o fim. - O padrão mais específico vence, qualquer que seja a ordem de registro. Padrões ambíguos causam panic no registro. Um caminho conhecido com o método errado recebe 405 e um cabeçalho
Allow, e um caminho desconhecido recebe 404. - Defina os cabeçalhos, depois chame
WriteHeader, depois escreva o corpo. O primeiroWriteenvia 200 e congela os cabeçalhos, e o que vier depois é ignorado. - As requisições rodam em goroutines próprias, então estado compartilhado num handler precisa de um mutex, e o
-raceencontra os lugares que não têm um. r.Context()é cancelado quando o cliente vai embora. Passe-o para trabalho lento.- Monte um
http.Servercom pelo menosReadHeaderTimeoutdefinido.http.ListenAndServeespera para sempre por clientes lentos.
Seu handler roda em muitas goroutines ao mesmo tempo, quer você as tenha iniciado, quer não.