Desenvolvimento de servidor Web em Golang - do simples ao complexo
Há cinco anos comecei desenvolver Gophish, isso proporcionou uma oportunidade de aprender Golang. Percebi que Go é uma linguagem poderosa, complementada por muitas bibliotecas. Go é versátil: em particular, pode ser usado para desenvolver aplicativos do lado do servidor sem problemas.
Este artigo é sobre como escrever um servidor em Go. Vamos começar com coisas simples como “Olá, mundo!” e terminar com um aplicativo com os seguintes recursos:
- Usando Let's Encrypt para HTTPS.
— Trabalhando como roteador API.
— Trabalhando com middleware.
— Processamento de arquivos estáticos.
— Desligamento correto.
Depois disso, se você executar o aplicativo e abrir a página localhost, você verá imediatamente o texto “Olá, mundo!” (se tudo funcionar corretamente, claro).
Usaremos o manipulador diversas vezes posteriormente, mas primeiro vamos entender como tudo funciona.
net / http
O exemplo usou o pacote net/http, é a principal ferramenta do Go para desenvolver servidores e clientes HTTP. Para entender o código, vamos entender o significado de três elementos importantes: http.Handler, http.ServeMux e http.Server.
Manipuladores HTTP
Quando recebemos uma solicitação, o manipulador a analisa e gera uma resposta. Os manipuladores em Go são implementados da seguinte forma:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
O primeiro exemplo usa a função auxiliar http.HandleFunc. Ele envolve outra função, que por sua vez leva http.ResponseWriter e http.Request para ServeHTTP.
Em outras palavras, os manipuladores em Golang são apresentados em uma única interface, o que oferece muitas opções ao programador. Assim, por exemplo, o middleware é implementado usando um manipulador, onde o ServeHTTP primeiro faz algo e depois chama o método ServeHTTP de outro manipulador.
Conforme mencionado acima, os manipuladores simplesmente geram respostas às solicitações. Mas qual manipulador específico deve ser usado em um determinado momento?
Solicitar roteamento
Para fazer a escolha certa, use um multiplexador HTTP. Em várias bibliotecas ele é chamado de muxer ou roteador, mas são todos a mesma coisa. A função do multiplexador é analisar o caminho da solicitação e selecionar o manipulador apropriado.
Se você precisar de suporte para roteamento complexo, é melhor usar bibliotecas de terceiros. Alguns dos mais avançados - gorila/mux и go-chi/chi, essas bibliotecas permitem implementar o processamento intermediário sem problemas. Com a ajuda deles, você pode configurar o roteamento curinga e executar diversas outras tarefas. Sua vantagem é a compatibilidade com manipuladores HTTP padrão. Como resultado, você pode escrever código simples que pode ser modificado no futuro.
Trabalhar com estruturas complexas em uma situação normal exigirá soluções não padronizadas e isso complica significativamente o uso de manipuladores padrão. Para criar a grande maioria dos aplicativos, uma combinação da biblioteca padrão e um roteador simples será suficiente.
Processamento de consultas
Além disso, precisamos de um componente que “escute” as conexões recebidas e redirecione todas as solicitações para o manipulador correto. http.Server pode facilmente realizar essa tarefa.
A seguir mostra que o servidor é responsável por todas as tarefas relacionadas ao processamento da conexão. Isso, por exemplo, funciona usando o protocolo TLS. Para implementar a chamada http.ListenAndServer, um servidor HTTP padrão é usado.
Agora vamos ver exemplos mais complexos.
Adicionando Let's Encrypt
Por padrão, nosso aplicativo é executado no protocolo HTTP, mas é recomendado usar o protocolo HTTPS. Isso pode ser feito sem problemas no Go. Se você recebeu um certificado e uma chave privada, basta registrar ListenAndServeTLS com o certificado e os arquivos de chave corretos.
Let’s Encrypt fornece certificados gratuitos com renovação automática. Para utilizar o serviço, você precisa de um pacote autocert.
A maneira mais fácil de configurá-lo é usar o método autocert.NewListener em combinação com http.Serve. O método permite obter e atualizar certificados TLS enquanto o servidor HTTP processa solicitações:
Se abrirmos no navegador example.com, receberemos uma resposta HTTPS “Olá, mundo!”
Se precisar de uma configuração mais detalhada, você deve usar o gerenciador autocert.Manager. Em seguida, criamos nossa própria instância http.Server (até agora a usamos por padrão) e adicionamos o gerenciador ao servidor TLSConfig:
Esta é uma maneira fácil de implementar suporte HTTPS completo com renovação automática de certificado.
Adicionando rotas personalizadas
O roteador padrão incluído na biblioteca padrão é bom, mas é muito básico. A maioria dos aplicativos exige roteamento mais complexo, incluindo rotas aninhadas e curinga, ou um procedimento para definir padrões e parâmetros de caminho.
Neste caso vale a pena usar pacotes gorila/mux и go-chi/chi. Aprenderemos como trabalhar com este último - um exemplo é mostrado abaixo.
É fornecido o arquivo api/v1/api.go contendo rotas para nossa API:
/ HelloResponse is the JSON representation for a customized message
type HelloResponse struct {
Message string `json:"message"`
}
// HelloName returns a personalized JSON message
func HelloName(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
response := HelloResponse{
Message: fmt.Sprintf("Hello %s!", name),
}
jsonResponse(w, response, http.StatusOK)
}
// NewRouter returns an HTTP handler that implements the routes for the API
func NewRouter() http.Handler {
r := chi.NewRouter()
r.Get("/{name}", HelloName)
return r
}
Definimos o prefixo api/vq para rotas no arquivo principal.
Podemos então montá-lo em nosso roteador principal sob o prefixo api/v1/ em nosso aplicativo principal:
// NewRouter returns a new HTTP handler that implements the main server routes
func NewRouter() http.Handler {
router := chi.NewRouter()
router.Mount("/api/v1/", v1.NewRouter())
return router
}
http.Serve(autocert.NewListener("example.com"), NewRouter())
A facilidade do Go em trabalhar com rotas complexas torna possível simplificar a estruturação e manutenção de aplicações grandes e complexas.
Trabalhando com middleware
A preparação envolve agrupar um manipulador HTTP com outro, possibilitando executar rapidamente autenticação, compactação, registro e diversas outras funções.
Como exemplo, vejamos a interface http.Handler; vamos usá-la para escrever um manipulador que autentica os usuários do serviço.
func RequireAuthentication(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isAuthenticated(r) {
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
return
}
// Assuming authentication passed, run the original handler
next.ServeHTTP(w, r)
})
}
Existem roteadores de terceiros, como o chi, que permitem estender a funcionalidade do middleware.
Trabalhando com arquivos estáticos
A biblioteca padrão Go inclui recursos para trabalhar com conteúdo estático, incluindo imagens, JavaScript e arquivos CSS. Eles podem ser acessados através da função http.FileServer. Ele retorna um manipulador que serve arquivos de um diretório específico.
Definitivamente, vale lembrar que http.Dir exibe o conteúdo do diretório se ele não contiver o arquivo index.html principal. Neste caso, para evitar que o diretório seja comprometido, você deve usar o pacote unindexed.
Desligamento correto
Go também possui um recurso chamado desligamento normal do servidor HTTP. Isso pode ser feito usando o método Shutdown(). O servidor é iniciado em uma goroutine e então o canal é escutado para receber um sinal de interrupção. Assim que o sinal é recebido, o servidor desliga, mas não imediatamente, mas após alguns segundos.
handler := server.NewRouter()
srv := &http.Server{
Handler: handler,
}
go func() {
srv.Serve(autocert.NewListener(domains...))
}()
// Wait for an interrupt
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
// Attempt a graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
Como conclusão
Go é uma linguagem poderosa com uma biblioteca padrão quase universal. Seus recursos padrão são muito amplos e podem ser aprimorados usando interfaces - isso permite desenvolver servidores HTTP verdadeiramente confiáveis.