Un middleware es una pieza de software que asiste a una aplicación para interactuar o comunicarse con otras aplicaciones, paquetes de programas, redes, hardware o sistemas operativos. Funciona como una capa de abstracción de software distribuida, que se sitúa entre las capas de aplicaciones y las capas inferiores abstrayendo de la complejidad y heterogeneidad de las redes de comunicaciones subyacentes, proporcionando una API para la fácil programación y manejo de aplicaciones distribuidas.
Bien, ya hemos repasado qué es un middleware según la Wikipedia. Sin embargo, esa definición es un poco vaga (o ambigua), así que tendremos que buscar una definición más acotada para poder ceñirnos a la longitud de un artículo del blog. Así que, por qué no hablar de las funciones middleware?
¿Qué es un middleware?
En términos de arquitectura del software, efectivamente, un middleware es un componente en nuestras aplicaciones que nos va a permitir centrarnos en la lógica de negocio aislándonos de las complejidades del bajo nivel. Por ejemplo, en los frameworks o librerías HTTP se suele hablar de funciones middleware, definidas como “funciones que tienen acceso a la petición HTTP (request), al objeto de la respuesta (response) y a la siguiente función middleware, y que intervienen en el ciclo de petición-respuesta de la aplicación (HTTP)"
De este modo, las funciones middleware en un contexto HTTP pueden tener propósitos muy amplios y diferentes, desde modificar la propia petición HTTP (añadiendo headers, des/comprimiendo el contenido, etc) hasta modificar la respuesta que se va a devolver como resultado de dicha petición, pasando por aspectos como la autorización y la autenticación del usuario que hace la petición, el error handling, el logging, etc.
¿Qué es un middleware en Go?
Entonces, de qué hablamos generalmente cuándo nos referimos a middleware en el contexto de Go?
Pues, aunque en determinados momentos podemos hacer referencia a otros aspectos, como veremos más adelante y en futuros
artículos, en general nos vamos a referir ni más ni menos que a la siguiente definición que podemos encontrar en el paquete
net/http
:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Que como podemos ver, se trata de una interfaz que encaja bastante con lo que vimos anteriormente. Es decir, una función
que va a recibir tanto la petición como el objeto de la respuesta y que hará algo (o no) con ellos. Sin embargo, es
habitual que cuándo definamos nuestros middlewares, los queramos definir como una función con dichos argumentos, sin
tener que implementar una estructura de datos con el método ServeHTTP
de forma explícita. Para ello, vamos a usar
el adapter que nos proporciona el mismo paquete:
type HandlerFunc func(ResponseWriter, *Request)
Vamos, un simple type alias o named type, como se suele llamar en la comunidad.
De esta forma, podríamos definir nuestros handlers HTTP haciendo uso del método net.Handle
.
del siguiente modo:
http.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}))
En esta ocasión, nuestro handler básicamente escribiría “hello” en el body de la respuesta.
¿Handlers o middlewares?
Si habéis prestado atención, os habréis fijado que hemos ido mezclado los términos handler y middleware. De forma intencionada, por supuesto. Pues, al final, un handler HTTP, en términos generales se trata de eso, un componente que es capaz de manejar o manipular peticiones HTTP para devolver una respuesta.
Sin embargo, si repescamos la definición de middleware anterior, nos daremos cuenta de que a los handlers que hemos visto hasta ahora les falta uno de los tres componentes. Tenemos la petición (request), tenemos el objeto de la respuesta (o el objeto encargado de la misma -writer-), pero aún nos falta tener la próxima función middleware.
Por esa sencilla razón, lo que vamos a hacer es que nuestros handlers no sean definidos como funciones anónimas, ni
tampoco como funciones que cumplan la firma del adapter que vimos
anteriormente, sino que vamos a hacer que nuestros handlers estén definidos dentro de funciones generadoras. Es decir,
de una función cuyo retorno es precisamente un http.Handler
.
Con esa definición, podríamos refactorizar el código anterior del siguiente modo:
func helloHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
}
http.Handle("/hello", helloHandler())
Inyección de dependencias
Esto (como ya habrán visto algunos), lo que nos va a permitir es aplicar el patrón de diseño de inyección de dependencias.
De forma que ahora nuestro handler podría ser reutilizado para la escritura de diferentes mensajes de respuesta:
func messageHandler(message string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(message))
})
}
http.Handle("/hello", messageHandler("hello"))
http.Handle("/goodbye", messageHandler("goodbye"))
Con los beneficios que eso conlleva, claro.
Además, ahora ya seremos capaces de “evolucionar” nuestros handlers para que se conviertan en middlewares, dándoles el parámetro que les faltaba. Veamos como hacerlo:
func messageMiddleware(message string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
w.Write([]byte(message))
})
}
Como podemos ver, ahora podríamos hacer múltiples operaciones tanto antes como después de llamar al siguiente middleware. Esto nos podría servir, por ejemplo, para tomar métricas de cuánto tiempo tarda nuestro sistema en devolver una respuesta. Ahí va otro ejemplo:
func logResponseTimeMiddleware(message string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t0 := time.Now()
next.ServeHTTP(w, r)
log.Println(time.Since(t0))
})
}
Entonces, para cerrar ese debate de cómo debemos nombrar a nuestros componentes, podríamos definir uno en función del otro y viceversa:
Un middleware es un handler que recibe otro middleware (o handler) al cuál llamará antes y/o después de hacer unas determinadas operaciones. Y, un handler es un middlware que NO recibe otro middleware (o handler). Por lo tanto, podríamos decir que el middleware final de ese ciclo de vida petición-respuesta es propiamente el handler y que todos los handlers previos son middlewares. Pero al final, como hemos podido ver con los ejemplos anteriores, la premisa básica es la misma: recibir una petición (request) y el objeto encargado de la respuesta (response) y realizar operaciones interactuando (o no) con ellos.
Y ahora sí, ya podemos decir que sabemos a qué hacemos referencia cuándo hablamos de middlewares en Go.
Los middlewares y la comunidad
Como podéis suponer, y como vimos anteriormente, los middlewares no dejan de ser código reutilizable que puede ser compartido entre diferentes proyectos. Es por eso que es habitual ver repositorios con colecciones de middlewares, tanto genéricos como para implementaciones específicas.
Por ejemplo, si usáis la librería mux
que presentamos en el artículo sobre cómo
crear tú primera API REST en Go, podéis hacer uso de cualquiera de los
middlewares que la misma gente de Gorilla tiene en su repositorio.
Del mismo modo que si utilizáis el framework Gin, podéis hacer uso de cualquiera de los
middlewares que tienen disponibles en su repositorio de contribuciones.
Y eso no es todo, pues no os debéis conformar con lo que hay por ahí publicado, sinó que podéis usar dichas contribuciones a modo de ejemplo o inspiración para hacer vuestras propias implementaciones. Por que, ahora seríais capaces de hacerlo, ¿verdad?
No solo de HTTP y REST se vive
Ya para terminar, solo queríamos dejar claro que, tal y como vimos desde el principio, el concepto de middleware puede ser tan genérico o agnóstico como nosotros queramos, es por eso que dentro del ecosistema de gRPC nos podemos encontrar también repositorios con colecciones de middlewares (como éste), o interceptors como se les llama en dicho ámbito.
Desde hace ya un tiempo, en Friends of Go estamos trabajando para publicar nuestra propia colección de middlewares.
¿Y vosotros? ¿Tenéis alguna? No hace falta que sea vuestra, también podéis compartir las de otros!
Como siempre, estaremos encantados de recibir vuestro feedback en los comentarios del blog o vía Twitter.