Conceptos Básicos de Canales en Golang

GolangGolangBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Una de las características más atractivas del lenguaje Go es su soporte nativo para la programación concurrente. Tiene un fuerte soporte para la paralelización, la programación concurrente y la comunicación de red, lo que permite un uso más eficiente de los procesadores multi-core.

A diferencia de otros lenguajes que logran la concurrencia a través de la memoria compartida, Go logra la concurrencia utilizando canales (channels) para la comunicación entre diferentes goroutines.

Las goroutines son las unidades de concurrencia en Go y se pueden considerar como hilos (threads) livianos. Consumen menos recursos al cambiar entre ellas en comparación con los hilos tradicionales.

Los canales (channels) y las goroutines juntos constituyen los primitivos de concurrencia en Go. En esta sección, aprenderemos sobre los canales, esta nueva estructura de datos.

Puntos de conocimiento:

  • Tipos de canales (channels)
  • Creación de canales (channels)
  • Operaciones en canales (channels)
  • Bloqueo de canales (channel blocking)
  • Canales unidireccionales (unidirectional channels)

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("Golang")) -.-> go/BasicsGroup(["Basics"]) go(("Golang")) -.-> go/DataTypesandStructuresGroup(["Data Types and Structures"]) go(("Golang")) -.-> go/ConcurrencyGroup(["Concurrency"]) go/BasicsGroup -.-> go/values("Values") go/BasicsGroup -.-> go/variables("Variables") go/DataTypesandStructuresGroup -.-> go/structs("Structs") go/ConcurrencyGroup -.-> go/goroutines("Goroutines") go/ConcurrencyGroup -.-> go/channels("Channels") go/ConcurrencyGroup -.-> go/select("Select") subgraph Lab Skills go/values -.-> lab-149096{{"Conceptos Básicos de Canales en Golang"}} go/variables -.-> lab-149096{{"Conceptos Básicos de Canales en Golang"}} go/structs -.-> lab-149096{{"Conceptos Básicos de Canales en Golang"}} go/goroutines -.-> lab-149096{{"Conceptos Básicos de Canales en Golang"}} go/channels -.-> lab-149096{{"Conceptos Básicos de Canales en Golang"}} go/select -.-> lab-149096{{"Conceptos Básicos de Canales en Golang"}} end

Resumen de los Canales (Channels)

Los canales (channels) son una estructura de datos especial utilizada principalmente para la comunicación entre goroutines.

A diferencia de otros modelos de concurrencia que utilizan mutexes y funciones atómicas para acceder de forma segura a los recursos, los canales (channels) permiten la sincronización al enviar y recibir datos entre múltiples goroutines.

En Go, un canal (channel) es como una cinta transportadora o una cola que sigue la regla de Primero en Entrar, Primero en Salir (First-In-First-Out, FIFO).

Tipos y Declaración de Canales (Channels)

Un canal (channel) es un tipo de referencia, y su formato de declaración es el siguiente:

var channelName chan elementType

Cada canal tiene un tipo específico y solo puede transportar datos del mismo tipo. Veamos algunos ejemplos:

Crea un archivo llamado channel.go en el directorio ~/project:

cd ~/project
touch channel.go

Por ejemplo, declaremos un canal de tipo int:

var ch chan int

El código anterior declara un canal de enteros (int) llamado ch.

Además de esto, hay muchos otros tipos de canales comúnmente utilizados.

En channel.go, escribe el siguiente código:

package main

import "fmt"

func main() {
    var ch1 chan int   // Declare an integer channel
    var ch2 chan bool  // Declare a boolean channel
    var ch3 chan []int // Declare a channel that transports slices of integers
    fmt.Println(ch1, ch2, ch3)
}

El código anterior simplemente declara tres tipos diferentes de canales.

Ejecuta el programa utilizando el siguiente comando:

go run channel.go

La salida del programa es la siguiente:

<nil> <nil> <nil>

Inicialización de Canales (Channels)

Similar a los mapas, los canales (channels) necesitan ser inicializados después de ser declarados antes de poder ser utilizados.

La sintaxis para inicializar un canal es la siguiente:

make(chan elementType)

En channel.go, escribe el siguiente código:

package main

import "fmt"

func main() {
    // Channel that stores integer data
    ch1 := make(chan int)

    // Channel that stores boolean data
    ch2 := make(chan bool)

    // Channel that stores []int data
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

Ejecuta el programa utilizando el siguiente comando:

go run channel.go

La salida del programa se muestra a continuación:

0xc0000b4000 0xc0000b4040 0xc0000b4080

En el código anterior, se definen e inicializan tres tipos diferentes de canales.

Al examinar la salida, podemos ver que, a diferencia de los mapas, imprimir directamente el canal en sí no revela su contenido.

Imprimir directamente un canal solo muestra la dirección de memoria del propio canal. Esto se debe a que, como un puntero, un canal es solo una referencia a una dirección de memoria.

Entonces, ¿cómo podemos acceder y operar sobre los valores en un canal?

Operaciones en Canales (Channels)

Envío de Datos

Las operaciones en canales (channels) tienen sus propias formas únicas. En general, cuando trabajamos con otros tipos de datos, usamos = e índices para acceder y manipular los datos.

Pero para los canales, usamos <- tanto para las operaciones de envío como de recepción.

En un canal, escribir datos se llama enviar, lo que significa enviar un dato al canal.

Entonces, ¿cómo enviamos datos?

En channel.go, escribe el siguiente código:

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    fmt.Println(ch)
}

Ejecuta el programa utilizando el siguiente comando:

go run channel.go

La salida del programa es la siguiente:

0xc0000b4000

Aunque ponemos datos en el canal, no podemos verlos directamente en la salida del valor del canal.

Esto se debe a que los datos en el canal aún deben ser recibidos antes de poder ser utilizados.

Recepción de Datos

¿Qué significa recibir de un canal? Cuando aprendemos sobre mapas o slices, podemos recuperar cualquier dato de estos tipos de datos basándonos en índices o claves.

En el caso de los canales, solo podemos recuperar el valor más antiguo que ha entrado en el canal.

Después de que el valor es recibido, ya no existirá en el canal.

En otras palabras, recibir de un canal es como consultar y eliminar datos en otros tipos de datos.

En channel.go, escribe el siguiente código:

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Ejecuta el programa utilizando el siguiente comando:

go run channel.go

La salida del programa es la siguiente:

10
20

Podemos ver que el valor 10, que entró primero en el canal, se muestra primero, y después de que se muestra, ya no existe en el canal.

El valor 20, que entró después, se mostrará después, lo que destaca el principio de primero en entrar, primero en salir (First-In-First-Out, FIFO) del canal.

Cierre del Canal

Después de que un canal ya no es necesario, debe cerrarse. Sin embargo, al cerrar un canal, debemos prestar atención a los siguientes puntos:

  • Enviar un valor a un canal cerrado causará un error en tiempo de ejecución.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
    
        fmt.Println(<-ch)
    
        close(ch)
        ch <- 20
    }

    La salida del programa es la siguiente:

    10
    panic: send on closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:10 +0x1a0
    exit status 2
  • Recibir de un canal cerrado continuará recuperando valores hasta que esté vacío.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    Ejecuta el programa utilizando el siguiente comando:

    go run channel.go

    La salida del programa es la siguiente:

    10
    20
    30
  • Recibir de un canal cerrado y vacío recibirá el valor inicial predeterminado del tipo de dato correspondiente.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    Ejecuta el programa utilizando el siguiente comando:

    go run channel.go

    La salida del programa es la siguiente:

    10
    20
    30
    0
    0
  • Cerrar un canal cerrado lanzará un error.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
        close(ch)
        close(ch)
        fmt.Println(<-ch)
    }

    La salida del programa es la siguiente:

    panic: close of closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:9 +0x75
    exit status 2

Bloqueo de Canales (Channels)

Los estudiantes atentos pueden haber notado que cuando introdujimos las declaraciones e inicialización de canales (channels), no especificamos la capacidad del canal:

package main

import "fmt"

func main() {
    // Stores integer data in the channel
    ch1 := make(chan int) // Same below

    // Stores boolean data in the channel
    ch2 := make(chan bool)

    // Stores []int data in the channel
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

Pero cuando demostramos las operaciones de canales, especificamos la capacidad del canal:

package main

import "fmt"

func main() {
    // Specify the channel capacity as 3
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Para los canales sin capacidad especificada, se les llama canales sin búfer (unbuffered channels), que no tienen espacio de búfer.

Si un emisor o receptor no está listo, la primera operación se bloqueará hasta que la otra operación esté lista.

chan1 := make(chan int) // Unbuffered channel of type int

Para los canales con capacidad y espacio de búfer especificados, se les llama canales con búfer (buffered channels).

chan2 := make(chan int, 5) // Buffered channel of type int with a capacity of 5

Funcionan como una cola, siguiendo la regla de Primero en Entrar, Primero en Salir (First In First Out, FIFO).

Para los canales con búfer, podemos usar operaciones de envío para agregar elementos al final de la cola y usar operaciones de recepción para eliminar elementos del frente de la cola.

¿Qué sucede si ponemos más datos en un canal con búfer que su capacidad? Vamos a comprobarlo. Escribe el siguiente código en el archivo channel1.go:

cd ~/project
touch channel1.go
package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    ch <- 20
    fmt.Println("succeed")
}

Ejecuta el programa utilizando el siguiente comando:

go run channel1.go

La salida del programa es la siguiente:

fatal error: all goroutines are asleep - deadlock!

Descubrimos que el programa entra en un bloqueo (deadlock) porque el canal ya está lleno.

Para resolver este problema, podemos extraer los datos del canal primero. Escribe el siguiente código en channel.go:

package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    fmt.Println("Data extracted:", <-ch)

    ch <- 20
    fmt.Println("succeed")
}

Ejecuta el programa utilizando el siguiente comando:

go run channel.go

La salida del programa es la siguiente:

Data extracted: 10
succeed

Podemos usar continuamente el canal con una capacidad limitada adoptando una estrategia de tomar, usar, tomar y usar de nuevo.

Canales Unidireccionales (Unidirectional Channels)

Por defecto, un canal (channel) es bidireccional y se puede leer o escribir en él. A veces, necesitamos restringir el uso de un canal, por ejemplo, queremos asegurarnos de que un canal solo se pueda escribir o solo se pueda leer dentro de una función. ¿Cómo podemos lograr esto?

Podemos declarar explícitamente un canal con uso restringido antes de inicializarlo.

Canal de Solo Escritura (Write-only Channel)

Un canal de solo escritura significa que dentro de una función determinada, el canal solo se puede escribir y no se puede leer. Declaremos y usemos un canal de solo escritura de tipo int:

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 10
    fmt.Println("Data written to channel")
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    fmt.Println("Data received:", <-ch)
}

Ejecuta el programa utilizando el siguiente comando:

go run channel.go

La salida del programa es la siguiente:

Data written to channel
Data received: 10

En este ejemplo, la función writeOnly restringe el canal a solo escritura.

Canal de Solo Lectura (Read-only Channel)

Un canal de solo lectura significa que dentro de una función determinada, el canal solo se puede leer y no se puede escribir. Declaremos y usemos un canal de solo lectura de tipo int:

package main

import "fmt"

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    ch <- 20
    readOnly(ch)
}

Ejecuta el programa utilizando el siguiente comando:

go run channel.go

La salida del programa es la siguiente:

Data received from channel: 20

En este ejemplo, la función readOnly restringe el canal a solo lectura.

Combinación de Canales de Solo Escritura y Solo Lectura

Puedes combinar canales de solo escritura y solo lectura para demostrar cómo fluyen los datos a través de canales restringidos:

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 30
    fmt.Println("Data written to channel")
}

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    readOnly(ch)
}

Ejecuta el programa utilizando el siguiente comando:

go run channel.go

La salida del programa es la siguiente:

Data written to channel
Data received from channel: 30

Este ejemplo ilustra cómo un canal de solo escritura puede pasar datos a un canal de solo lectura.

Resumen

En este laboratorio, aprendimos los conceptos básicos de los canales (channels), que incluyen:

  • Tipos de canales y cómo declararlos
  • Métodos para inicializar canales
  • Operaciones en canales
  • El concepto de bloqueo de canales
  • Declaración de canales unidireccionales