Enrutador y Middleware en Go con Gorilla Mux

En un post anterior vimos cómo responder peticiones HTTP con rutas en Go, pero las mismas no eran tan simples cuando se trataba de variables en la URL o métodos HTTP.

Hoy vamos a ver un enrutador o router de Go, que permite definir rutas y métodos HTTP para responder a ellos, de una manera fácil.

El enrutador, llamado Mux (que es de las herramientas de Gorilla) permite:

  • Definir middleware en las rutas, es decir, aplicar funciones que se ejecutan antes de cada petición HTTP y que permiten detener la ejecución o loguear determinadas cosas
  • Definición de rutas con verbos HTTP
  • Lectura de parámetros GET
  • Lectura de variables dentro de la url. Por ejemplo si definimos algo como usuario/{id} y se consulta a usuario/1 podemos obtener el valor 1 accediendo a la variable
  • Variables dentro de la URL con expresiones regulares

En resumen, Mux es un router de Go que soporta además Middleware. Veamos algunos ejemplos del mismo.

Si quieres ver el código final míralo en GitHub.

Nota: si usas PHP te recomendo Phroute.

Instalar Go y Gorilla mux

Comienza instalando Go en Windows o Linux, después de eso instala la librería con:

go get github.com/gorilla/mux

Crear y definir una ruta

Hay que importar la librería y después crear el enrutador:

package main

import (
	"github.com/gorilla/mux"
)

func main() {
	// Crear el enrutador y definir las rutas en la función definirRutas
	enrutador := mux.NewRouter()
}

Eso solo crea el enrutador. Para agregarle handlers o manejadores de rutas invocamos a la función HandleFunc que recibe dos argumentos:

  1. La URL a la que responderá
  2. Una función que va a manejar la petición. Dicha función es un handler de HTTP y tiene como argumentos la petición y la respuesta HTTP.

Veamos un ejemplo:

enrutador.HandleFunc("/hola", func(respuesta http.ResponseWriter, peticion *http.Request) {
  respuesta.Write([]byte("¡Hola, mundo!"))
}).Methods("GET")

Cuando se visite la url /hola se llamará a esa función. Dentro de esa función podemos invocar a más código que consulte datos o haga algo y después escriba la respuesta HTTP.

Se puede decir que este es nuestro pegamento o controlador si conocemos MVC.

No es necesario indicar el método, pero lo pongo para que se vea cómo se puede indicar a qué método HTTP responderá la petición (GET)

También se pueden crear middlewares, pero eso lo veremos más adelante.

El servidor HTTP combinado con el enrutador

El enrutador solo mapea las URL de las peticiones, pero debemos iniciar un servidor para que las mismas sean servidas.

En Go es muy fácil definir un servidor. Lo configuramos para que el handler sea el enrutador que acabamos de crear, y llamamos al método ListenAndServe()

package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"time"
)

func main() {
	// Crear el enrutador y definir las rutas en la función definirRutas
	enrutador := mux.NewRouter()
	
        // aquí definir todas las rutas...
        //enrutador.HandleFunc...
  
	// Dirección del servidor. En este caso solo indicamos el puerto
	// pero podría ser algo como "127.0.0.1:8000"
	direccion := ":8000"

	servidor := &http.Server{
		Handler: enrutador,
		Addr:    direccion,
		// Timeouts para evitar que el servidor se quede "colgado" por siempre
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	fmt.Printf("Escuchando en %s. Presiona CTRL + C para salir", direccion)
	log.Fatal(servidor.ListenAndServe())
}

Definir el handler de manera separada

No es necesario complicar el código de esa manera (personalmente lo veo limpio pero alguien puede diferir), ya que podemos definir antes el handler y después pasarlo como argumento, teniendo un código más limpio:

enrutador.HandleFunc("/usuarios", obtenerUsuarios).Methods("GET")

func obtenerUsuarios(respuesta http.ResponseWriter, peticion *http.Request) {
  // Nota: estos usuarios podrían venir de una base de datos que podrían obtenerse dentro
  // de cada ruta
  type Usuario struct {
    Id     int    `json:"id"`
    Correo string `json:"correo"`
  }
	
  var usuarios []Usuario = []Usuario{
    Usuario{
      Id:     1,
      Correo: "contacto@parzibyte.me",
    },
    Usuario{
      Id:     2,
      Correo: "john.galt@atlas.com",
    },
  }
  // También podrías codificar otro tipo de datos como un arreglo plano
  // o una simple variable, todo lo soportado por JSON:
  // https://parzibyte.me/blog/2019/05/16/codificar-decodificar-json-go-golang/
  json.NewEncoder(respuesta).Encode(usuarios)
}

En la primera línea definimos la ruta, pero definimos la función manejadora de manera aparte. No te compliques por la función, lo único que hace es devolver un JSON de usuarios.

Variables en la URL

Mux permite leer las variables dentro de la URL de manera sencilla a través del método mux.Vars() que recibe la petición y devuelve un mapa de la variables.

Veamos un ejemplo en donde se consultan las ventas de determinado tipo y año:

enrutador.HandleFunc("/ventas/{tipo}/{anio}", func(respuesta http.ResponseWriter, peticion *http.Request) {
  variablesDePeticion := mux.Vars(peticion)
  tipo := variablesDePeticion["tipo"]
  anio := variablesDePeticion["anio"]
  respuesta.Write([]byte("El tipo de venta es " + tipo + " y el año es " + anio))
}).Methods("GET")

Es tan fácil como acceder al mapa de variables. Todas las variables son recibidas como cadenas, pero se pueden convertir a entero con distintos métodos dependiendo del tipo de dato.

En el siguiente ejemplo se convierte una cadena a entero, la cadena es el id de un usuario que se quiere recuperar:

enrutador.HandleFunc("/usuario/{id}", obtenerUsuarioPorId).Methods("GET")

func obtenerUsuarioPorId(respuesta http.ResponseWriter, peticion *http.Request) {
	variablesDePeticion := mux.Vars(peticion)
	// El id viene como cadena, hay que convertirlo a entero de 32 bits
	// Aquí "id" es la variable que indicamos en la ruta
	idUsuarioBuscado, err := strconv.Atoi(variablesDePeticion["id"])
        // TODO: hacer algo con la variable
}

Variables en la URL con expresión regular

Ahora veamos cómo atrapar las variables pero solo si cumplen una expresión regular. Para ello definimos la expresión después de nombrar la variable, separando ambas partes con dos puntos:

enrutador.HandleFunc("/pedidos/{anio:[0-9]{4}}", func(respuesta http.ResponseWriter, peticion *http.Request) {
	variablesDePeticion := mux.Vars(peticion)
	anio := variablesDePeticion["anio"]
	respuesta.Write([]byte("Pedidos del año: " + anio))
}).Methods("GET")

La expresión regular indica que solo tomará en cuenta lo que sea formado con dígitos del 0 al 9 y que tenga 4 cifras. Lo sé, no funcionará para el año 10000 pero para los años actuales funciona y solo es un ejemplo.

También podemos tener opciones fijas:

enrutador.HandleFunc("/libros/{orden:(?:ascendente|descendente)}", func(respuesta http.ResponseWriter, peticion *http.Request) {
  variablesDePeticion := mux.Vars(peticion)
  orden := variablesDePeticion["orden"]
  respuesta.Write([]byte("Libros ordenados: " + orden))
}).Methods("GET")

Podemos separar las opciones con el pipe |. Es importante poner el ?: para que no sea un grupo de captura y se confunda al hacer expresiones regulares.

Parámetros GET

Los parámetros GET de una ruta son aquellos que van separados por un ampersand y un signo de interrogación al inicio. Por ejemplo:

http://localhost:8000/cursos?orden=ascendente&orden=descendente&busqueda_titulo=atlas&busqueda_autor=ayn

Para manejarlos debemos acceder a los valores según la documentación de Go, pues Mux no tiene nada que ver con esto.

La ventaja es que podemos usar los valores GET y las rutas al mismo tiempo. Así:

enrutador.HandleFunc("/cursos", func(respuesta http.ResponseWriter, peticion *http.Request) {
	variablesGet := peticion.URL.Query()
	// Cada variable es un arreglo
	orden := variablesGet["orden"]
	// Si mide más que 0 entonces el orden sí está definido
	if len(orden) > 0 {
		fmt.Fprintf(respuesta, "El orden: %s.", orden[0]) // Acceder al primer elemento
	}
	fmt.Fprintf(respuesta, "Parámetros de consulta: %v", variablesGet)
}).Methods("GET")

Se accede a los valores a través de peticion.URL.Query; eso devolverá un mapa de arreglos. Para acceder al valor hay que comprobar la longitud del arreglo, ya que si el valor no está presente el arreglo medirá cero.

Más métodos: PUT, POST, DELETE

Hasta ahora todos los métodos que hemos visto son GET. Veamos cómo manejar una petición POST, la cual trae un cuerpo:

enrutador.HandleFunc("/usuario", agregarUsuario).Methods("POST")
func agregarUsuario(respuesta http.ResponseWriter, peticion *http.Request) {
	
	type Usuario struct {
	  Id     int    `json:"id"`
	  Correo string `json:"correo"`
	}
	
	var usuarioNuevo Usuario
	// Intenta decodificar el cuerpo de la petición (peticion.Body) dentro de usuario (&usuario)
	err := json.NewDecoder(peticion.Body).Decode(&usuarioNuevo)
	if err != nil {
		json.NewEncoder(respuesta).Encode("Cuerpo de petición no válido")
		return
	}
	// Si el usuario era válido lo agregamos al arreglo
	usuarios = append(usuarios, usuarioNuevo)
	json.NewEncoder(respuesta).Encode(usuarioNuevo)
}

El primer cambio importante es que ahora llamamos a Method con POST.

El segundo es que definimos un struct para leer un JSON. Ese JSON debe ser enviado cuando se solicite la ruta, y debe coincidir con el struct.

Si la lectura es exitosa podemos trabajar con esa variable, guardarla o hacer algo con ella.

Los métodos más comunes quedan definidos así:

	enrutador.HandleFunc("/usuarios", obtenerUsuarios).Methods("GET")
	enrutador.HandleFunc("/usuario/{id}", obtenerUsuarioPorId).Methods("GET")
	enrutador.HandleFunc("/usuario", agregarUsuario).Methods("POST")
	enrutador.HandleFunc("/usuario", actualizarUsuario).Methods("PUT")
	enrutador.HandleFunc("/usuario/{id}", eliminarUsuario).Methods("DELETE")

Al final del post dejaré un ejemplo completo, no te preocupes si no defino las funciones por ahora.

Middleware con Gorilla Mux

Un Middleware es algo que permite que todas las peticiones pasen por un lugar antes de ser servidas.

El Middleware puede decidir atender la petición o rechazarla, o simplemente agregarle datos y dejar que el siguiente método se encargue.

Es decir, el Middleware es algo que está en medio. Como ejemplo básico tenemos un middleware que loguea cada petición. Queda así:

func middlewareLog(siguienteManejador http.Handler) http.Handler {
	return http.HandlerFunc(
		func(respuesta http.ResponseWriter, peticion *http.Request) {
			log.Printf("Nueva petición. Método: %s. IP: %s. URL solicitada: %s\n",
				peticion.Method, peticion.RemoteAddr, peticion.URL)
			siguienteManejador.ServeHTTP(respuesta, peticion)
		})
}

Es una función simple que devuelve un http.HandlerFunc, las mismas funciones que hemos estado viendo. Lo interesante está en el parámetro que recibe: un http.Handler.

Middleware en Go

Como es un middleware de log no vamos a detener o continuar la petición, así que invocamos el siguiente manejador con siguienteManejador.ServeHTTP pasándole la respuesta y la petición.

Es de gran importancia mencionar que si no se llama al siguiente manejador, la petición se detiene ahí mismo o mejor dicho se cancela.

Con eso definimos el middleware, pero no lo estamos usando. Para usarlo debemos invocar el método Use de nuestro enrutador.

enrutador.Use(middlewareLog)

Podemos definir muchos middlewares y serán llamados en el orden en el que llamemos a Use.

Veamos otro ejemplo de Middleware que ahora sí detiene o deja continuar la ejecución de la petición:

func middlewareEliminar(siguienteManejador http.Handler) http.Handler {
	return http.HandlerFunc(
		func(respuesta http.ResponseWriter, peticion *http.Request) {
			// Si no llamamos a siguienteManejador, se detiene
			// así que podemos aquí comprobar algo y detener determinada acción
			// Por ejemplo, permitir solo si no son de tipo DELETE
			if peticion.Method == http.MethodDelete {
				http.Error(respuesta, "Permiso denegado", http.StatusForbidden)
			} else {
				// En caso de que sea permitida llamamos a siguienteManejador
				// y le pasamos la respuesta con la petición
				siguienteManejador.ServeHTTP(respuesta, peticion)
			}
		})
}

El funcionamiento de este middleware es muy simple: si el método de la petición es DELETE entonces genera un error y detiene la ejecución de la petición.

Si el método no es DELETE entonces llama al siguiente handler. Lo hice así de simple para demostrar cómo se usa, pero podemos comparar otras cosas, obtener sesiones, encabezados, llamar a bases de datos, etcétera.

Encerrar la definición de rutas en otro archivo

Es una buena práctica definir nuestras rutas  y middlewares en un archivo distinto a donde creamos el servidor, así lo tenemos separado y el mantenimiento es más rápido.

Mi archivo de rutas completo queda como se ve a continuación. Para probar estoy trabajando con arreglos fijos, pero solo son ejemplos, no te preocupes mucho por el funcionamiento interno, sino por las rutas.

package main

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"strconv"
)

type Usuario struct {
	Id     int    `json:"id"`
	Correo string `json:"correo"`
}

// Nota: estos usuarios podrían venir de una base de datos que podrían obtenerse dentro
// de cada ruta

var usuarios []Usuario = []Usuario{
	Usuario{
		Id:     1,
		Correo: "contacto@parzibyte.me",
	},
	Usuario{
		Id:     2,
		Correo: "john.galt@atlas.com",
	},
}

func definirRutas(enrutador *mux.Router) {
	// Rutas para ejemplificar variables
	enrutador.HandleFunc("/ventas/{tipo}/{anio}", func(respuesta http.ResponseWriter, peticion *http.Request) {
		variablesDePeticion := mux.Vars(peticion)
		tipo := variablesDePeticion["tipo"]
		anio := variablesDePeticion["anio"]
		respuesta.Write([]byte("El tipo de venta es " + tipo + " y el año es " + anio))
	}).Methods("GET")

	enrutador.HandleFunc("/pedidos/{anio:[0-9]{4}}", func(respuesta http.ResponseWriter, peticion *http.Request) {
		variablesDePeticion := mux.Vars(peticion)
		anio := variablesDePeticion["anio"]
		respuesta.Write([]byte("Pedidos del año: " + anio))
	}).Methods("GET")

	enrutador.HandleFunc("/libros/{orden:(?:ascendente|descendente)}", func(respuesta http.ResponseWriter, peticion *http.Request) {
		variablesDePeticion := mux.Vars(peticion)
		orden := variablesDePeticion["orden"]
		respuesta.Write([]byte("Libros ordenados: " + orden))
	}).Methods("GET")

	enrutador.HandleFunc("/cursos", func(respuesta http.ResponseWriter, peticion *http.Request) {
		variablesGet := peticion.URL.Query()
		// Cada variable es un arreglo
		orden := variablesGet["orden"]
		// Si mide más que 0 entonces el orden sí está definido
		if len(orden) > 0 {
			fmt.Fprintf(respuesta, "El orden: %s.", orden[0]) // Acceder al primer elemento
		}
		fmt.Fprintf(respuesta, "Parámetros de consulta: %v", variablesGet)
		// respuesta.Write([]byte( + variablesGet))
	}).Methods("GET")

	enrutador.HandleFunc("/usuarios", obtenerUsuarios).Methods("GET")
	enrutador.HandleFunc("/usuario/{id}", obtenerUsuarioPorId).Methods("GET")
	enrutador.HandleFunc("/usuario", agregarUsuario).Methods("POST")
	enrutador.HandleFunc("/usuario", actualizarUsuario).Methods("PUT")
	enrutador.HandleFunc("/usuario/{id}", eliminarUsuario).Methods("DELETE")
}

func middlewareLog(siguienteManejador http.Handler) http.Handler {
	return http.HandlerFunc(
		func(respuesta http.ResponseWriter, peticion *http.Request) {
			log.Printf("Nueva petición. Método: %s. IP: %s. URL solicitada: %s\n",
				peticion.Method, peticion.RemoteAddr, peticion.URL)
			siguienteManejador.ServeHTTP(respuesta, peticion)
		})
}
func middlewareEliminar(siguienteManejador http.Handler) http.Handler {
	return http.HandlerFunc(
		func(respuesta http.ResponseWriter, peticion *http.Request) {
			// Si no llamamos a siguienteManejador, se detiene
			// así que podemos aquí comprobar algo y detener determinada acción
			// Por ejemplo, permitir solo si no son de tipo DELETE
			if peticion.Method == http.MethodDelete {
				http.Error(respuesta, "Permiso denegado", http.StatusForbidden)
			} else {
				// En caso de que sea permitida llamamos a siguienteManejador
				// y le pasamos la respuesta con la petición
				siguienteManejador.ServeHTTP(respuesta, peticion)
			}
		})
}

func agregarUsuario(respuesta http.ResponseWriter, peticion *http.Request) {
	var usuarioNuevo Usuario
	// Intenta decodificar el cuerpo de la petición (peticion.Body) dentro de usuario (&usuario)
	err := json.NewDecoder(peticion.Body).Decode(&usuarioNuevo)
	if err != nil {
		json.NewEncoder(respuesta).Encode("Cuerpo de petición no válido")
		return
	}
	// Si el usuario era válido lo agregamos al arreglo
	usuarios = append(usuarios, usuarioNuevo)
	json.NewEncoder(respuesta).Encode(usuarioNuevo)
}

func eliminarUsuario(respuesta http.ResponseWriter, peticion *http.Request) {
	// Nota: en realidad no se elimina, porque la implementación para eliminar
	// de un arreglo es un poco difícil de explicar y no es necesario para los propósitos
	// del código. Se hace lo mismo que en la función obtener, pero quería demostrar el método DELETE
	variablesDePeticion := mux.Vars(peticion)
	// El id viene como cadena, hay que convertirlo a entero de 32 bits
	// Aquí "id" es la variable que indicamos en la ruta
	idUsuarioBuscado, err := strconv.Atoi(variablesDePeticion["id"])
	// Si no es un entero válido:
	if err != nil {
		json.NewEncoder(respuesta).Encode("Error: id inválido")
		return
	}

	// Nota: el id puedes usarlo para filtrar en una base de datos o algo así,
	// aquí simplemente lo buscamos dentro del arreglo

	// Buscamos...
	for _, usuario := range usuarios {
		// Si lo encontramos lo devolvemos y terminamos la función
		if usuario.Id == idUsuarioBuscado {
			json.NewEncoder(respuesta).Encode(usuario)
			return
		}
	}
	// Si no lo encontramos, indicamos un error
	json.NewEncoder(respuesta).Encode("No existe un usuario con el id proporcionado")
}

func actualizarUsuario(respuesta http.ResponseWriter, peticion *http.Request) {
	var usuarioActualizado Usuario
	// Intenta decodificar el cuerpo de la petición (peticion.Body) dentro de usuario (&usuario)
	err := json.NewDecoder(peticion.Body).Decode(&usuarioActualizado)
	if err != nil {
		json.NewEncoder(respuesta).Encode("Cuerpo de petición no válido")
		return
	}
	// Ya tenemos al usuario, ahora lo actualizamos en el arreglo.
	// Para ello debemos obtener el índice
	for indice, usuarioExistente := range usuarios {
		// Si lo encontramos lo devolvemos y terminamos la función
		if usuarioExistente.Id == usuarioActualizado.Id {
			usuarios[indice] = usuarioActualizado
			json.NewEncoder(respuesta).Encode(usuarioActualizado)
			return
		}
	}
	// Si no lo encontramos por Id entonces ese usuario no existía
	json.NewEncoder(respuesta).Encode("Usuario no encontrado")
}

func obtenerUsuarios(respuesta http.ResponseWriter, peticion *http.Request) {
	// También podrías codificar otro tipo de datos como un arreglo plano
	// o una simple variable, todo lo soportado por JSON:
	// https://parzibyte.me/blog/2019/05/16/codificar-decodificar-json-go-golang/
	json.NewEncoder(respuesta).Encode(usuarios)
}

func obtenerUsuarioPorId(respuesta http.ResponseWriter, peticion *http.Request) {
	variablesDePeticion := mux.Vars(peticion)
	// El id viene como cadena, hay que convertirlo a entero de 32 bits
	// Aquí "id" es la variable que indicamos en la ruta
	idUsuarioBuscado, err := strconv.Atoi(variablesDePeticion["id"])
	// Si no es un entero válido:
	if err != nil {
		json.NewEncoder(respuesta).Encode("Error: id inválido")
		return
	}

	// Nota: el id puedes usarlo para filtrar en una base de datos o algo así,
	// aquí simplemente lo buscamos dentro del arreglo

	// Buscamos...
	for _, usuario := range usuarios {
		// Si lo encontramos lo devolvemos y terminamos la función
		if usuario.Id == idUsuarioBuscado {
			json.NewEncoder(respuesta).Encode(usuario)
			return
		}
	}
	// Si no lo encontramos, indicamos un error
	json.NewEncoder(respuesta).Encode("No existe un usuario con el id proporcionado")
}

Poniendo todo junto y compilando

El archivo main.go queda así:

package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"log"
	"net/http"
	"time"
)

func main() {
	// Crear el enrutador y definir las rutas en la función definirRutas
	enrutador := mux.NewRouter()
	definirRutas(enrutador)
	enrutador.Use(middlewareLog)
	enrutador.Use(middlewareEliminar)
	// Dirección del servidor. En este caso solo indicamos el puerto
	// pero podría ser algo como "127.0.0.1:8000"
	direccion := ":8000"

	servidor := &http.Server{
		Handler: enrutador,
		Addr:    direccion,
		// Timeouts para evitar que el servidor se quede "colgado" por siempre
		WriteTimeout: 15 * time.Second,
		ReadTimeout:  15 * time.Second,
	}
	fmt.Printf("Escuchando en %s. Presiona CTRL + C para salir", direccion)
	log.Fatal(servidor.ListenAndServe())
}

Recuerda que las rutas quedaron arriba. Ahora se compila con:

go build

Y se ejecuta el archivo resultante. Al visitar localhost las rutas funcionan como en las imágenes mostradas.

Conclusión

Dejo la documentación oficial y el enlace al repositorio en donde está todo el código.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *