08. Go web: closures en Golang

Publicado por

Hasta ahora, cada vez que nuestro servidor invoca un manejador, es necesario extraer el título de la wiki que pide el cliente y verificar que no hay errores. Para lograrlo agregamos las siguientes líneas a cada manejador:

titulo, err := dameTitulo(w, r)

if err != nil {
  return
}

En esta entrada te voy a mostrar cómo modificar tu servidor para prescindir de esas líneas.

Conceptos previos

Antes de continuar es necesario que conozcas lo que es una closure (clausura, en español). Si ya conoces el término, puedes saltar a la siguiente sección.

Jaakko Järvi, John Freeman y Lawrence Crowl nos dicen en su publicación Lambda Expressions and Closures: “una closure consiste en el código del cuerpo de la función lambda y el ámbito en que esta fue creada”. Podemos definirlas de forma muy burda como funciones “almacenadas” que recuerdan el ámbito o entorno en que fueron hechas.

Para comprender este concepto es necesario poner unos ejemplos. Analicemos la siguiente función:

func suma(a int) func(int) int{
  return func(b int) int{
    return a + b
  }
}

Esa es la forma de crear closures en Golang; la función suma() recibe un argumento a int y devuelve una función anónima (sin identificador) que a su vez esta espera un argumento b int y devuelve un int. Podemos utilizar la función suma() para crear y llamar closures como a continuación:

suma01 := suma(5) //Creamos una closure
suma02 := suma(10) //Creamos otra

fmt.Println("Suma 01: ", suma01(2)) //Suma 5 + 2
fmt.Println("Suma 02: ", suma02(100)) //Suma 10 + 100

Salida:

Suma 01: 7
Suma 02: 110

En las primeras dos líneas hacemos que suma01 y suma02 almacenen una closure, es decir, una función y el ámbito en que fue creada. A pesar de haber salido del ámbito de suma(), ambas closures almacenan la variable a int correspondiente al momento de su creación (suma01 tiene a=5, suma02 tiene a=10), cuando se invoca la función de las closures sumamos a + b (el a del momento de la creación, y el b que pasamos al invocarlas). Es posible reutilizar y hacer la cantidad de llamados que queramos a una closure.

Otra gran ventaja es que si la función que necesitamos devolver es muy corta o solo se repite su llamado un par de veces en el programa, podemos crearlas en una sola línea como en seguida te muestro:

saludar := func(nombre string) string { return "¡Hola, " + nombre + "!"}

Cada que llamemos la closure saludar le necesitamos pasar una string:

fmt.Println(saludar("José"))
fmt.Println(saludar("María"))

Así logramos imprimir mensajes como los siguientes:

¡Hola, José!
¡Hola, María!

Closures en el servidor

Después de repasar rápidamente lo que es una closure, es momento de agregarlas a nuestro servidor para eliminar las repeticiones innecesarias de código. Comenzamos por modificar los manejadores para que reciban un nuevo argumento tipo string, además eliminamos los llamados a dameTitulo() y la validación de posibles errores al hacerlo. Los manejadores quedarían así:

//Manejador para mostrar wikis
func manejadorMostrar(w http.ResponseWriter, r *http.Request, titulo string){
  p, err := cargarPagina(titulo)

  if err != nil {
    http.Redirect(w, r, "/edit/" + titulo, http.StatusFound)
    fmt.Println("La página solicitada no existía. Llamando al editor...")
    return
  }
  cargarPlantilla(w, "view", p)
}

//Manejador para editar wikis
func manejadorEditar(w http.ResponseWriter, r *http.Request, titulo string){
  p, err := cargarPagina(titulo)
  if err != nil{
    p = &Pagina{Titulo: titulo}
  }
  cargarPlantilla(w, "edit", p)
}

//Manejador para guardar wikis
func manejadorGuardar(w http.ResponseWriter, r * http.Request, titulo string) {
  cuerpo := r.FormValue("body")
  p := &Pagina{Titulo: titulo, Cuerpo: []byte(cuerpo)}
  fmt.Println("Guardando " + titulo + ".txt...")
  p.guardar()
  http.Redirect(w, r, "/view/" + titulo, http.StatusFound)
}

Ahora creamos una función que recibirá una función del tipo de los manejadores como argumento y devolverá una closure de tipo http.HandlerFunc() la cual será pasada a la función http.HandleFunc():

func llamarManejador(manejador func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
    titulo, err := dameTitulo(w, r)
    if err != nil {
      http.NotFound(w, r)
      return
    }
    manejador(w, r, titulo)
  }
}

Para poder utilizar funciones como argumentos, necesitamos especificar su tipo, es decir, el tipo de los argumentos y de los valores que devuelve. La mayoría del cuerpo de la función anterior ya ha sido revisado en notas anteriores: con la función dameTitulo() extraemos el título de la petición y lo validamos con la expresión regular (regex) que añadimos anteriormente, si es un título inválido escribimos un error a ResponseWriter, en cambio, si el título es correcto, se hará un llamado a la función adjunta que se le pasó de argumento (al manejador de edición, de guardado o de mostrar).

Modificamos la función principal para que haga llamados a llamarManejador() con el manejador que corresponda para cada caso (para hacer más limpio el código, quitamos las líneas con las que creábamos una página de ejemplo en las versiones anteriores del servidor):

func main() {
  http.HandleFunc("/view/", llamarManejador(manejadorMostrar))
  http.HandleFunc("/save/", llamarManejador(manejadorGuardar))
  http.HandleFunc("/edit/", llamarManejador(manejadorEditar))

  http.ListenAndServe(":8080", nil)
}

Para compilar y ejecutar:

go build servidor_wikis.go
./servidor_wikis

Para verificar que el servidor funciona ingresamos a:

http://localhost:8080/cualquier_pagina_que_desees

Finalizando…

Espero que lo visto en esta entrada te haya servido, cualquier duda puedes dejarla en la sección de comentarios. Hasta la próxima. See ya!

Deja una respuesta

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