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:
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.
Comienza instalando Go en Windows o Linux, después de eso instala la librería con:
go get github.com/gorilla/mux
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:
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 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())
}
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.
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
}
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.
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.
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.
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
.
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.
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")
}
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.
Dejo la documentación oficial y el enlace al repositorio en donde está todo el código.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.