Jesús Mendoza

Notas de Go - Concurrencia, Goroutines y Channels

Concurrencia

La mayoría de nuestros programas están compuestos por funciones. Por ejemplo, un servidor web está compuesto por funciones que manejan cada petición y se encargan de enviar una respuesta. Normalmente esas pequeñas funciones no se ejecutan de manera simultanea, es decir, si recibimos varias peticiones a nuestro servidor web tenemos que esperar a que la primera función que maneja la primera petición se termine de ejecutar para poder ejecutar la siguiente función. Lo ideal sería que dichos subprogramas se ejecutaran al mismo tiempo para que todas las respuestas sean ejecutadas lo más rápido posible.

El proceso de hacer que los subprogramas se ejecuten al mismo tiempo se llama concurrencia. Go soporta concurrencia usando goroutines y channels.

Goroutines

Son funciones o métodos que son capaces de ejecutarse concurrente (al mismo tiempo) que otras funciones se ejecutan. Son una versión más ligera de hilos (threads), por lo que es común ver programas de Go con muchas Goroutines.

Ventajas de las Goroutines

  • Son muy ligeras en comparación con crear un nuevo thread. Pesan solo unos kbs y pueden crecer o hacerse más pequeñas dependiendo de la necesidad de nuestro programa, en cambio el tamaño de los threads es fijo y tiene que ser especificado.
  • Puede haber un thread de nuestro SO ejecutando muchas goroutines pero si alguna bloquea ese thread entonces otro threah es creado en nuestro SO y el resto de goroutines que no han sido ejecutadas se mueven a ese nuevo thread. Todo esto lo maneja el motor de Go, así que nosotros como programadores no tenemos que preocuparnos por hacer todo esto.
  • Las Goroutines se comunican entre sí usando channels. Los channels están diseñados para prevenir "race conditions" cuando accedemos a espacios de memoria compartidos.

Todos los programas empiezan con una goroutine explicita que es la función main pero para crear una nueva goroutine tenemos que agregar el prefijo go a la hora de ejecutar nuestra función. Por ejemplo:

package main

import "fmt"

func hello() {
  fmt.Println("Hello world from a Goroutine")
}

func main() {
  go hello()

  fmt.Println("Hello from main")
}

Si ejecutamos el ejemplo anterior vemos que estamos ejecutando la función go hello() esto crea una goroutine pero nuestro programa se ejecuta y solo imprime "Hello from main". No hay ningún problema con nuestro código, es totalmente normal que no veamos "Hello world from a Goroutine". El motivo por el que sucede esto es que nuestro programa ejecuta la función go hello() de forma concurrente, es decir, ejecuta la función go hello() y luego sigue ejecutando el resto de métodos y funciones dentro de la función main, el problema es que nuestro programa llega a la linea que imprime "Hello from main" y ve que ya no tiene más nada para ejecutar y se cierra.

Si queremos hacer que nuestro programa espere a que se terminen de ejecutar nuestras goroutine tenemos que aprender un poco sobre channels.

Channels

Los channels son la forma en la que nos podemos comunicar con nuestras goroutine. Son como una tubería que envía datos de un lado a otro.

Cada channel tiene un tipo asociado a él, este tipo es el tipo de datos que podremos comunicar a través de él. El valor 0 de los channels es nil. Para crear un channel tenemos que usar la función make. Por ejemplo:

package main

import "fmt"

func main() {
  canalInt := make(chan int)
  canalString := make(chan string)

  fmt.Printf("%T", canalInt)
  fmt.Printf("%T", canalString)
}

En el ejemplo anterior estamos creando un channel de tipo int usando make(chan int) que si imprimimos su tipo verémos que nos devuelve chan int. Y otro channel de tipo string que si imprimimos su tipo verémos que nos devuelve chan string.

Enviando y recibiendo datos desde un channel

Para enviar datos desde un channel usamos la siguiente sintaxis:

data := <- a

En el codigo anterior recibimos los datos del channel a y se lo asignamos a la variable usando `data := <- a.

Para enviar datos a un channel usamos la siguiente sintaxis:

a <- data

En el código anterior decimos que queremos enviar el valor de data a nuestro channel a usando a <- data.

Una forma fácil de entender este código es ver hacia donde apunta la flecha <-. Si apunta a nuestro channel estamos enviado datos y si apunta desde nuestro channel estamos recibiendo datos.

Cerrando un channel

Para prevenir que nuestro programa tenga memory leaks tenemos que cerrar nuestros channels una vez dejemos de usarlos. Para cerrar un channel tenemos que usar la palabra close. Por ejemplo:

package main

import (
    "fmt"
)

func print(c chan string) {
    c <- "Hello, from a Goroutine"

    close(c)
}

func main() {
    canal := make(chan string)

    go print(canal)

    if value, ok := <- canal; !ok {
        fmt.Println("Error closing the channel")
    } else {
        fmt.Println(value)
    }
}

Como podemos observar en este caso nuestro channel retorna dos valores, value y ok. El ok lo podemos usar para verificar que nuestro channel se ha cerrado correctamente. En nuestra función print estamos recibiendo un channel y al terminar de enviar información a este estamos usando la función close(c) para cerrar nuestro channel.