Desarrollo de servidores web en Golang — de lo simple a lo complejo

Desarrollo de servidores web en Golang — de lo simple a lo complejo

Hace cinco años comencé a desarrollar Gophish, lo que me dio la oportunidad de aprender Golang. Me di cuenta de que Go es un lenguaje poderoso, cuyas capacidades se complementan con muchas bibliotecas. Go es versátil: en particular, se pueden desarrollar fácilmente aplicaciones del lado del servidor.

Este artículo se dedica a escribir un servidor en Go. Comenzaremos con cosas simples, como «¡Hola, mundo!», y terminaremos con una aplicación que tenga las siguientes capacidades:

— Uso de Let's Encrypt para HTTPS.
— Funciona como enrutador API.
— Trabaja con middleware.
— Manejo de archivos estáticos.
— Finalización adecuada.

Skillbox recomienda: Curso práctico «Desarrollador de Python desde cero».

Recordamos: para todos los lectores de «Habr» — un descuento de 10,000 rublos al registrarse en cualquier curso de Skillbox usando el código promocional «Habr».

¡Hola, mundo!

Se puede crear un servidor web en Go muy rápidamente. Aquí hay un ejemplo de un manejador que devuelve el mencionado «¡Hola, mundo!».

package main
 
import (
"fmt"
"net/http"
)
 
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "¡Hola, Mundo!")
})
http.ListenAndServe(":80", nil)
}

Después de esto, si inicias la aplicación y abres la página localhost, verás de inmediato el texto «¡Hola, mundo!» (por supuesto, si todo funciona correctamente).

A partir de aquí, utilizaremos el manejador repetidamente, pero primero comprendamos cómo funciona todo.

net/http

En el ejemplo se utilizó el paquete net/http, que es la herramienta principal en Go para desarrollar tanto servidores como clientes HTTP. Para entender el código, analicemos el significado de tres elementos importantes: http.Handler, http.ServeMux y http.Server.

Manejadores HTTP

Cuando recibimos una solicitud, el manejador la analiza y forma una respuesta. Los manejadores en Go se implementan de la siguiente manera:

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

En el primer ejemplo se utiliza la función auxiliar http.HandleFunc. Esta envuelve otra función que, a su vez, recibe http.ResponseWriter y http.Request en ServeHTTP.

En otras palabras, los manejadores en Golang están representados por una única interfaz, lo que ofrece muchas posibilidades para el programador. Por ejemplo, el middleware se implementa mediante un manejador, donde ServeHTTP primero hace algo y luego llama al método ServeHTTP de otro manejador.

Como se mencionó anteriormente, los manejadores simplemente generan respuestas a solicitudes. Pero, ¿qué manejador específico se debe usar en un momento determinado?

Enrutamiento de solicitudes

Para tomar la decisión correcta, utilice un multiplexor HTTP. En varias bibliotecas, se le denomina muxer o router, pero todo es lo mismo. La función del multiplexor consiste en analizar la ruta de la solicitud y seleccionar el manejador correspondiente.

Si se necesita soporte para enrutamiento complejo, entonces es mejor utilizar bibliotecas de terceros. Algunas de las más avanzadas son gorilla/mux y go-chi/chi, estas bibliotecas permiten implementar el procesamiento intermedio sin problemas. Con su ayuda, se puede configurar el enrutamiento con comodines y realizar varias otras tareas. Su ventaja es la compatibilidad con los manejadores HTTP estándares. Como resultado, puede escribir un código simple con la posibilidad de modificarlo en el futuro.

Trabajar con marcos complejos en situaciones normales requerirá soluciones no estándar, lo cual complica significativamente el uso de manejadores predeterminados. Para crear la mayoría de las aplicaciones, basta con una combinación de la biblioteca por defecto y un enrutador sencillo.

Procesamiento de solicitudes

Además, necesitamos un componente que 'escuche' las conexiones entrantes y redirija todas las solicitudes al manejador correcto. Esta tarea la puede llevar a cabo sin dificultad http.Server.

A continuación se muestra que el servidor es responsable de todas las tareas relacionadas con el procesamiento de conexiones. Esto incluye, por ejemplo, el trabajo con el protocolo TLS. Para implementar la llamada http.ListenAndServer se utiliza el servidor HTTP estándar.

Ahora revisemos ejemplos más complejos.

Adición de Let’s Encrypt

Por defecto, nuestra aplicación opera con el protocolo HTTP; sin embargo, se recomienda utilizar el protocolo HTTPS. En Go, esto se puede hacer sin problemas. Si ha obtenido un certificado y una clave privada, entonces es suficiente con especificar ListenAndServeTLS indicando los archivos de certificado y clave correctos.

http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)

Siempre se puede hacer mejor.

Let’s Encrypt ofrece certificados gratuitos con la posibilidad de actualizaciones automáticas. Para utilizar el servicio, se necesita el paquete autocert.

La forma más sencilla de configurarlo es utilizar el método autocert.NewListener en combinación con http.Serve. El método permite obtener y actualizar certificados TLS, mientras que el servidor HTTP maneja las solicitudes:

http.Serve(autocert.NewListener("example.com"), nil)

Si abrimos en el navegador example.com, entonces obtendremos una respuesta HTTPS «Hello, world!».

Si se necesita una configuración más detallada, es conveniente utilizar el gestor autocert.Manager. A continuación, creamos nuestra propia instancia http.Server (hasta ahora hemos utilizado la predeterminada) y añadimos el gestor a la configuración TLS del servidor:

m := &autocert.Manager{
Cache:      autocert.DirCache("golang-autocert"),
Prompt:     autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
}
server := &http.Server{
    Addr:      ":443",
    TLSConfig: m.TLSConfig(),
}
server.ListenAndServeTLS("", "")

Esta es una forma sencilla de implementar soporte completo para HTTPS con renovación automática del certificado.

Añadiendo rutas personalizadas

El enrutador predeterminado incluido en la biblioteca estándar es bueno, pero es muy simple. En la mayoría de las aplicaciones, se necesita una enrutación más compleja, incluyendo enrutamientos anidados y comodines o bien un procedimiento para establecer plantillas y parámetros de rutas.

En este caso, vale la pena utilizar paquetes gorilla/mux y go-chi/chi. Con este último aprenderemos a trabajar — a continuación se muestra un ejemplo.

Dado — el archivo api/v1/api.go, que contiene las rutas para nuestra 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
}

Establecemos un prefijo para las rutas api/vq en el archivo principal.

Podemos montarlo en nuestro enrutador principal bajo el prefijo api/v1/ de vuelta en nuestra aplicación 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())

La simplicidad en el manejo de rutas complejas en Go permite simplificar la estructuración al mantener grandes aplicaciones complejas.

Trabajo con middleware

En el caso de la intermediación, se utiliza un envolvimiento de un manejador HTTP dentro de otro, lo que permite realizar rápidamente autenticación, compresión, registro y algunas otras funciones.

Como ejemplo, consideremos la interfaz http.Handler, con la que escribiremos un manejador de autenticación de usuarios del servicio.

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
        }
        \/\/ Asumiendo que la autenticación ha pasado, ejecuta el manejador original
        next.ServeHTTP(w, r)
    })
}

Existen enrutadores de terceros, como chi, que permiten ampliar la funcionalidad de la intermediación.

Trabajo con archivos estáticos

La biblioteca estándar de Go incluye capacidades para trabajar con contenido estático, incluyendo imágenes, así como archivos JavaScript y CSS. Se puede acceder a ellos a través de la función http.FileServer. Esta devuelve un manejador que distribuye archivos desde un directorio específico.

func NewRouter() http.Handler {
    router := chi.NewRouter()
    r.Get("/{name}", HelloName)
 
// Configuración para servir archivos estáticos
staticPath, _ := filepath.Abs("..\/..\/static\/\")
fs := http.FileServer(http.Dir(staticPath))
    router.Handle("/*", fs)
    
    return r

Es importante recordar que http.Dir muestra el contenido del directorio si no hay un archivo principal index.html en él. En este caso, para prevenir la exposición del directorio, se debe usar el paquete no indexado.

Cierre correcto

En Go, existe una función para el cierre adecuado del servidor HTTP. Esto se puede hacer utilizando el método Shutdown(). El servidor se ejecuta en una goroutine, y luego se escucha un canal para recibir la señal de interrupción. Una vez recibida la señal, el servidor se apaga, pero no inmediatamente, sino después de unos segundos.

handler := server.NewRouter()
srv := &http.Server{
    Handler: handler,
}
 
go func() {
srv.Serve(autocert.NewListener(domains...))
}()
 
// Esperar a una interrupción
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
 
// Intentar un cierre elegante
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)

En conclusión

Go es un lenguaje poderoso con una biblioteca estándar prácticamente universal. Sus capacidades predeterminadas son bastante amplias, y se pueden mejorar mediante interfaces, lo que permite desarrollar servidores HTTP realmente confiables.

Skillbox recomienda:

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster