Uso de canales como mecanismos de comunicación

Completado

Un canal en Go es un mecanismo de comunicación entre goroutines. Recuerde que el enfoque de la simultaneidad en Go es: "No comunicarse compartiendo memoria, sino compartir memoria comunicando". Cuando necesite enviar un valor de una goroutine a otra, use canales. Veamos cómo funcionan y cómo puede empezar a usarlas para escribir programas Go simultáneos.

Sintaxis de los canales

Dado que un canal es un mecanismo de comunicación que envía y recibe datos, también tiene un tipo. Esto significa que podría enviar datos solo para el tipo admitido por el canal. Use la palabra clave chan como el tipo de datos de un canal, pero también debe especificar el tipo de datos que pasará a través del canal, como un tipo int.

Cada vez que declara un canal o desea especificar un canal como parámetro en una función, debe usar chan <type>, como chan int. Para crear un canal, use la función integrada make():

ch := make(chan int)

Un canal puede realizar dos operaciones: enviar datos y recibir datos. Para especificar el tipo de operación que tiene un canal, debe utilizar el operador de canal <-. Además, el envío de datos y la recepción de datos en los canales son operaciones de bloqueo. En breve descubrirá por qué.

Si quiere especificar que un canal solo envíe datos, use el operador <- después del canal. Si quiere que el canal reciba datos, use el operador <- antes del canal, como en estos ejemplos:

ch <- x // sends (or writes ) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded

Otra operación que puede usar en un canal es cerrar dicho canal. Para cerrar un canal, use la función integrada close():

close(ch)

Al cerrar un canal, indica que los datos no se enviarán de nuevo en ese canal. Si intenta enviar datos a un canal cerrado, el programa entrará en pánico. Y si intenta recibir datos de un canal cerrado, podrá leer todos los datos enviados. Cada "lectura" posterior devolverá un valor de cero.

Vuelva al programa que ha creado antes y use canales para quitar la funcionalidad de suspensión. En primer lugar, vamos a crear un canal de cadena en la función main, como se indica a continuación:

ch := make(chan string)

Y quitaremos la línea de suspensión time.Sleep(3 * time.Second).

Ahora, podemos usar canales para comunicarse entre goroutines. En lugar de imprimir el resultado en la función checkAPI, se refactorizará el código y ese mensaje se enviará por el canal. Para usar el canal desde esa función, debe agregar el canal como parámetro. La función checkAPI debe tener el siguiente aspecto:

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

Tenga en cuenta que es necesario usar la función fmt.Sprintf porque no quiere imprimir ningún texto, simplemente enviar texto con formato por el canal. Además, observe que usamos el operador <- después de la variable de canal para enviar datos.

Ahora debe cambiar la función main para enviar la variable de canal y recibir los datos para imprimirla, como se muestra a continuación:

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)

Observe cómo usamos el operador <- antes de que el canal indique que queremos leer datos del canal.

Cuando vuelva a ejecutar el programa, verá una salida similar a la siguiente:

ERROR: https://api.somewhereintheinternet.com/ is down!

Done! It took 0.007401217 seconds!

Al menos funciona sin una llamada a una función de suspensión, ¿no? Pero todavía no hace lo que queremos. Vemos la salida solo de una de las goroutines, pero creamos cinco. En la siguiente sección descubriremos por qué este programa funciona de esta manera.

Canales no almacenados en búfer

Cuando se crea un canal mediante la función make(), se crea un canal no almacenado en búfer, que es el comportamiento predeterminado. Los canales no almacenados en búfer bloquean la operación de envío hasta que algún componente esté listo para recibir los datos. Como se ha afirmado antes, el envío y la recepción son operaciones de bloqueo. Esta operación de bloqueo también es la razón por la que el programa de la sección anterior se ha detenido en cuanto ha recibido el primer mensaje.

Podemos empezar diciendo que fmt.Print(<-ch) bloquea el programa porque está leyendo de un canal y espera a que lleguen algunos datos. En cuanto hay algunos, continúa con la línea siguiente y el programa finaliza.

¿Qué ha ocurrido con el resto de las goroutines? Todavía se están ejecutando, pero ya no hay ninguna escuchando. Y dado que el programa terminó pronto, algunas goroutines no pudieron enviar datos. Para demostrar esto, vamos a agregar otra línea fmt.Print(<-ch), como se indica a continuación:

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)

Cuando vuelva a ejecutar el programa, verá una salida similar a la siguiente:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!

Observe que ahora verá la salida de dos API. Si continúa agregando más líneas fmt.Print(<-ch), acabará leyendo todos los datos que se envían al canal. Pero ¿qué ocurre si intenta leer más datos y ya no hay ninguna goroutine que envíe datos? Por ejemplo:

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)

fmt.Print(<-ch)

Cuando vuelva a ejecutar el programa, verá una salida similar a la siguiente:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://dev.azure.com is up and running!

Funciona, pero el programa no finaliza. La última línea de impresión lo está bloqueando porque está esperando recibir datos. Tendrá que cerrar el programa con un comando como Ctrl+C.

El ejemplo anterior simplemente demuestra que la lectura y recepción de datos son operaciones de bloqueo. Para corregir este problema, podría cambiar el código a un bucle for y recibir solo los datos que sabe con certeza que va a enviar, como en este ejemplo:

for i := 0; i < len(apis); i++ {
    fmt.Print(<-ch)
}

Esta es la versión final del programa en caso de que algo ha ido mal con su versión:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

Cuando vuelva a ejecutar el programa, verá una salida similar a la siguiente:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
Done! It took 0.602099714 seconds!

El programa está haciendo lo que se supone que debe hacer. Ya no usa una función de suspensión; usa canales. Observe también que ahora se tardan aproximadamente 600 ms en finalizar en lugar de los casi 2 segundos cuando no se usaba la simultaneidad.

Por último, podríamos afirmar que los canales no almacenados en búfer están sincronizando las operaciones de envío y recepción. Aunque esté usando simultaneidad, la comunicación es sincrónica.