Menu

Servidor HTTP em Golang: rotas com net/http, JSON e códigos de status

Como construir um servidor web com o pacote padrão net/http do Go: handlers, rotas do ServeMux com métodos e wildcards no caminho (Go 1.22), respostas JSON, códigos de status, middleware, timeouts e desligamento gracioso.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Um servidor em poucas linhas

Um handler é uma função que recebe a requisição e escreve a resposta. O ServeMux encaminha as requisições para os handlers.

O editor não consegue aceitar conexões vindas do seu navegador, então os exemplos desta página iniciam o servidor com httptest.NewServer e o chamam a partir do mesmo programa. Em um programa real, a última parte é substituída por uma linha que bloqueia e serve para sempre:

log.Fatal(http.ListenAndServe(":8080", mux))

curl localhost:8080/hello/gopher imprime Hello, gopher!. O ListenAndServe só retorna em caso de erro (a porta já está em uso, por exemplo), e é por isso que ele fica dentro de um log.Fatal.

Cada requisição executa na sua própria goroutine. Tudo o que os seus handlers compartilham, como um map ou um contador, precisa de um mutex.

Handlers

Qualquer coisa com um método ServeHTTP(http.ResponseWriter, *http.Request) é um http.Handler. O http.HandlerFunc adapta uma função comum a essa interface, e o mux.HandleFunc faz a conversão por você. Um handler do tipo struct é prático quando os handlers precisam de dependências:

type API struct {
	db *sql.DB
}

func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }

mux.HandleFunc("GET /items", api.listItems)

O *http.Request oferece:

Campo ou métodoContém
r.MethodGET, POST, ...
r.URL.Patho caminho, /items/42
r.PathValue("id")um wildcard do padrão da rota (Go 1.22)
r.URL.Query().Get("q")um parâmetro da query string
r.Header.Get("Authorization")um header da requisição
r.Bodyo corpo da requisição, um io.ReadCloser (o servidor o fecha)
r.FormValue("name")um campo de formulário ou parâmetro de query
r.Context()um context cancelado quando o cliente se desconecta

Padrões de rota (Go 1.22)

Desde o Go 1.22, os padrões do ServeMux têm a forma [MÉTODO ][HOST]/[CAMINHO], e os caminhos podem conter wildcards.

PadrãoCasa com
"/items/"/items/ e tudo abaixo dele (barra no final = prefixo)
"/items"/items
"GET /items/{id}"GET (e HEAD) em /items/42; r.PathValue("id") == "42"
"POST /items"POST em /items
"/files/{path...}"/files/a/b/c; path é "a/b/c"
"/{$}"/, não qualquer caminho
"/"todo caminho que nenhum outro padrão casa

Quando dois padrões casam, o mais específico vence, então /items/new ganha de /items/{id}. Se nenhum for mais específico, por exemplo /items/{id} e /{kind}/new (os dois casam com /items/new), registrar o segundo causa panic com uma mensagem que nomeia os dois padrões. Se um caminho casa mas o método não, o mux responde 405 Method Not Allowed com um header Allow, sem nenhum código seu.

O padrão "/" pega tudo. Isso surpreende quem registra uma página inicial com "/" e descobre que ela responde 200 para toda URL desconhecida. Use "GET /{$}" para a página inicial.

Esses padrões exigem go 1.22 ou mais no go.mod. Com uma linha de versão mais antiga, o mux volta ao comportamento antigo: ele lê "GET /items" como um nome de host seguido de um caminho, então a rota nunca casa e toda requisição para /items recebe 404.

Uma pequena API JSON

A última requisição usa DELETE, que nenhuma rota aceita, e o mux responde 405 com Allow: GET, HEAD sozinho: esses são os métodos registrados para aquele caminho (uma rota GET também aceita HEAD).

Códigos de status e a ordem das escritas

Uma resposta tem três partes, e elas precisam ser escritas em ordem: headers, status, corpo.

  1. w.Header().Set(...) altera os headers. Só tem efeito até o status ser enviado.
  2. w.WriteHeader(code) envia a linha de status e os headers.
  3. w.Write(...) (ou fmt.Fprint(w, ...), ou um encoder) envia o corpo. Se WriteHeader não foi chamado, o primeiro Write envia 200 OK antes.

Consequências:

  • Definir um header depois que o corpo começou não faz nada.
  • Chamar WriteHeader duas vezes registra http: superfluous response.WriteHeader call e mantém o primeiro status.
  • Depois de uma resposta de erro, faça return. O http.Error não interrompe o seu handler, e o código depois dele continua escrevendo na mesma resposta.

Use as constantes com nome (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError) em vez de números puros. http.StatusText(404) devolve "Not Found".

Middleware

Um middleware é uma função que recebe um handler e devolve um handler, fazendo algo antes ou depois de chamar o próximo:

A linha log: sempre aparece antes de client got: para a mesma requisição. A goroutine do handler a imprime antes de retornar, e o servidor só conclui a resposta quando o handler retorna.

O statusRecorder embute o ResponseWriter real e sobrescreve só o WriteHeader, que é o jeito padrão de observar o código de status em um middleware.

Configurações de produção

O http.ListenAndServe usa um servidor sem timeouts, então um cliente lento pode manter uma conexão aberta indefinidamente. Configure um http.Server explicitamente para qualquer coisa exposta à internet, e desligue-o de forma graciosa:

srv := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 5 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      30 * time.Second,
	IdleTimeout:       120 * time.Second,
}

go func() {
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
	log.Println("shutdown:", err)
}

O Shutdown para de aceitar conexões novas e espera as requisições ativas terminarem, até o prazo do context. O ListenAndServe devolve http.ErrServerClosed assim que o Shutdown começa, e é por isso que esse erro não é tratado como falha. Para handlers de longa duração, passe r.Context() adiante para que eles parem quando o cliente for embora.

Servir arquivos estáticos é uma linha: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).

Erros comuns

  • Não retornar depois de http.Error. O handler continua executando e escreve mais coisas na resposta.
  • Definir headers depois de escrever o corpo. Eles são descartados em silêncio.
  • Registrar a página inicial em "/". Ela vira a rota que pega todo caminho desconhecido. Use "/{$}".
  • Compartilhar estado entre handlers sem lock. As requisições executam de forma concorrente.
  • Usar o servidor padrão em produção. Ele não tem timeouts; defina-os em um http.Server.
  • Padrões de rota ignorados. Se "GET /x/{id}" nunca casa, confira se o go.mod diz go 1.22 ou mais.

Perguntas frequentes

Como criar um servidor web simples em Go?

Registre um handler e comece a escutar: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) e depois log.Fatal(http.ListenAndServe(":8080", nil)). O servidor da biblioteca padrão serve para produção; ele trata HTTP/1.1, HTTP/2 sobre TLS, keep-alive e uma goroutine por conexão.

Como pegar um parâmetro de caminho no net/http do Go?

Desde o Go 1.22, os padrões do ServeMux podem conter wildcards: registre mux.HandleFunc("GET /items/{id}", h) e leia o valor dentro do handler com r.PathValue("id"). Um {path...} no final casa com o resto do caminho. Antes do 1.22, você precisava dividir r.URL.Path por conta própria ou usar um roteador como o chi.

Preciso de um framework como o Gin para construir uma API REST em Go?

Não. Desde o Go 1.22, o net/http roteia por método e por parâmetros de caminho, o que cobria o principal motivo de as pessoas usarem roteadores. Com encoding/json para os corpos e pequenas funções de middleware para logs e autenticação, a biblioteca padrão basta para a maioria das APIs. Frameworks acrescentam conveniências como binding e validação de requisições.

Como definir o código de status em um handler HTTP em Go?

Chame w.WriteHeader(http.StatusCreated) antes de escrever o corpo. Se você escrever o corpo primeiro, o Go envia 200 OK automaticamente e um WriteHeader posterior é ignorado com a mensagem de log superfluous response.WriteHeader call. Defina os headers com w.Header().Set também antes do WriteHeader; para erros, http.Error(w, msg, code) faz tudo isso.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR