Manejo avanzado de channels en Go: len, cap, close y select
Resumen
¿Cómo trabajar efectivamente con los canales en Go?
Manejar canales en Go es una habilidad esencial para quienes buscan aprovechar al máximo la concurrencia. Los canales permiten la comunicación segura entre gorutinas, facilitando así la coordinación de tareas en paralelo. Pero ¿cómo interactuamos con estos canales y sus múltiples valores? Aquí proporcionaremos un recorrido por los conceptos y técnicas necesarios para dominar el uso de canales en Go.
¿Cómo inicializamos y operamos con los canales?
Primero, para crear un canal, utilizamos la función make y especificamos el tipo de dato que manejará. Por ejemplo, para manejar strings:
c :=make(chanstring)
La novedad es que, esta vez, podemos insertar múltiples mensajes simultáneamente en el canal. Para ello, simplemente enviamos los mensajes al canal:
c <-"mensaje uno"c <-"mensaje dos"
Al interactuar con canales, dos funcionalidades clave son len y cap. Len indica cuántos datos hay actualmente en el canal, mientras que cap muestra la cantidad máxima de datos que puede almacenar este canal.
¿Cómo cerrar y recorrer los datos de un canal?
Cerrar un canal es crucial una vez que ya no planeamos enviar más datos. Esto se logra con close, indicando al runtime que no se recibirá más información en ese canal. Este paso no solo es una buena práctica sino que optimiza la eficiencia del código:
close(c)
Para recorrer los datos en un canal, utilizamos range dentro de un bucle for. Esta técnica es útil para iterar sobre los datos emitiendo mensajes almacenados:
for msg :=range c { fmt.Println(msg)}
Es recomendable cerrar el canal antes de utilizar range para asegurarnos de que no siga esperando más datos.
¿Cómo manejamos múltiples canales?
Cuando se manipulan varios canales y no sabemos cuál responderá primero, el uso de select es indispensable. Para mostrar esto, consideremos dos canales que reciben emails:
Creamos una función que envía mensajes a estos canales:
funcmensaje(msg string, c chanstring){ c <- msg
}
Al emplear select dentro de un bucle for, podemos manejar la concurrencia de múltiples canales simultáneamente:
for i :=0; i <2; i++{select{case m1 :=<-email1: fmt.Println("Email recibido de email1:", m1)case m2 :=<-email2: fmt.Println("Email recibido de email2:", m2)}}
Consejo adicional
Cuando trabajes con múltiples canales, ten en cuenta la cantidad y el tipo de datos que cada uno manejará. Esto te ayudará a definir correctamente el ciclo for y garantizar que tu aplicación Go funcione de manera efectiva.
Recuerda, dominar estos conceptos te permitirá manejar concurrencia en Go de manera eficiente. No dudes en plantearte un proyecto donde puedas aplicar estos conocimientos, como un sistema de atención en un hospital, donde la concurrencia es fundamental para optimizar el flujo de trabajo. Estamos seguros de que con práctica y dedicación, podrás lograrlo.
Estoy trabajando en un proyecto donde se reciben datos del estado de una máquina vending(cantidad de productos, temperatura, etc), se debe guardar y analizar los datos para detectar posibles fallas. Go sería muy útil en el procesamiento de muchas máquinas.
C y Go tienen muchas similitudes, para el caso que describes es más común el uso de C, porque es el estandar de manejo de Hardware.
"Excelente ejercicio. Podríamos implementar diferentes enfoques:
Podríamos rellenar la vending machine mientras sigue funcionando. Teniendo un canal para recargar la VM. Esto sería para una máquina que tenga su bodega automatizada, así el cliente puede pedir su Coca-Cola mientras la máquina llena el cajón de Pepsi.
Podríamos simular el mecanismo de entregar un ítem con time.Sleep() y permitir que el usuario siga comprando. Un canal para este mecanismo, cuando haya pedido en la cola, lo lleva hasta el cliente mientras él sigue comprando.
Podemos manejar la contabilidad y el inventario mientras el sistema sigue en marcha."
package main
import"fmt"func message(text string, c chan string){ c <- text
}func main(){c:=make(chan string,2) c <-"Mensaje 1" c <-"Mensaje 2" fmt.Println(len(c),cap(c))// Range y closeclose(c)//c<-"Mensaje 3"formessage:= range c { fmt.Println(message)}// Selectemail1:=make(chan string)email2:=make(chan string) go message("mensaje 1", email1) go message("mensaje 2", email2)fori:=0; i <2; i++{ select {casem1:=<-email1: fmt.Println("Email recibido de email 1", m1)casem2:=<-email2: fmt.Println("Email recibido de email 2", m2)}}}
Hola, vengo a compartir como yo hice el close de mi channel; basicamente comparo si el tamaño de mi channel es mayor o igual la capacidad de este, de ser así se cierra. Lo comparto por que creo que así sería en un proyecto de la vida real, pero no se, si tu ya has trabajado con channels en tu trabajo, de ser así por favor comentame si estoy siendo acertado con mi código o no.
Creo que esto te cerraria el channel apenas se abra el ultimo, es decir puede que si ejecutes tu primer y segundo channel pero al abrir el tercero se va a cerrar automaticamente y ya no da tiempo a que haga algo
No creo que esto sea completamente necesario, tu especificas la cantidad de rutinas que vas a manejar conociendo la cantidad especifica de la misma, si no la conoces, probablemente sea mejor no especificarla inicialmente e ir actualizandola conforme vayas cambiando tu codigo.
Aunque esto podria evitar que algun otro desarrollador agregue mas rutinas a un mismo canal sin cambiar la capacidad del mismo.
A continuación dejo un ejemplo extraído de https://tour.golang.org/concurrency/5 al cual le he añadido comentarios explicando lo que sucede. Este ejemplo me ha ayudado a entender mejor como funciona el "go" y "select".
package main
import"fmt"func fibonacci(c, quit chan int){ x,y:=0,1for{ select {case c <- x: x, y = y, x+y
case<-quit: fmt.Println("quit")return}}}func main(){c:=make(chan int)quit:=make(chan int)// go func()... se lanza de manera concurrente:// la línea fmt.Println(<-c) queda en espera hasta que haya algo que leer de <-c.// Al ser lanzado concurrentemente, mientras dicha línea espera, se va a la ejecución de fibonacci(c, quit),// que guardará valores en c <- x tantas veces se produzca la espera de la línea fmt.Println(<-c).// Una vez sale del bucle, pasa a esperar case <-quit, que será satisfecho con quit <- 0, por lo que imprimirá "quit" y finalizará// el programa. go func(){fori:=0; i <10; i++{ fmt.Println(<-c)} quit <-0}()fibonacci(c, quit)}
Excelente explicación, es decir que podemos acceder a los valores del chanel desde cualquier scope del programa
Les comparto este ejercicio de concurrencia tomado del Tour de Go. Consiste en comparar dos árboles para saber si contienen los mismos elementos. En este ejercicio, hago los recorridos en forma concurrente utilizando channels.
La idea es recibir los nodos de los arboles en canales y luego ordenar los nodos para compararlos uno a uno.
package main
import("container/list""fmt""golang.org/x/tour/tree""sort")// Walk walks the tree t sending all values// from the tree to the channel ch.funcWalk(t *tree.Tree, ch chanint){ fmt.Println("Walking", t) queue := list.New() queue.PushBack(t)for queue.Len()>0{ current := queue.Back() queue.Remove(current)var currentTree = current.Value.(*tree.Tree) ch <- currentTree.Value
if currentTree.Left !=nil{ queue.PushBack(currentTree.Left)}if currentTree.Right !=nil{ queue.PushBack(currentTree.Right)}} fmt.Println("Finished Walking", t)}// Same determines whether the trees// t1 and t2 contain the same values.funcSame(t1, t2 *tree.Tree)bool{ chan1 :=make(chanint,10) chan2 :=make(chanint,10)deferclose(chan1)deferclose(chan2)goWalk(t1, chan1)goWalk(t2, chan2) arr1 :=make([]int,10) arr2 :=make([]int,10)for i :=0; i <10; i++{ arr1[i]=<-chan1
}for i :=0; i <10; i++{ arr2[i]=<-chan2
} sort.Ints(arr1) sort.Ints(arr2)for i :=0; i <10; i++{if arr1[i]!= arr2[i]{returnfalse}}returntrue}funcmain(){ tree10 := tree.New(1) anotherTree10 := tree.New(1) tree20 := tree.New(2) fmt.Println(tree10," == ", anotherTree10," true ==",Same(tree10, anotherTree10)) fmt.Println(tree10," == ", tree20,"false ==",Same(tree10, tree20))}
El codigo del programa con algunos apuntes:
package main
import "fmt"
//del ejemplo message SELECT
func message(text string, c chan string) {
c <- text
}
func main() {
//Maneja 2 datos a la vez
c := make(chan string, 2)
// Inserta 2 mensajes
c <- "Mensaje 1"
c <- "Mensaje 2"
// len: cuantos gorutines hay en un channel
// cap: cantidad maxima que puede almacenar ese channel
fmt.Println(len(c), cap(c))
// Range y close keywork//CLOSE: le dice al runtime de GO que va a cerrar el canal una vez dejas de usarlo y no recibe ningun otro dato adicionalclose(c)//c<-"Mensaje 3"//RANGE: Recorrer la lista, reccoriendo todos los mensajes que estan en ese chanel cformessage:= range c { fmt.Println(message)}// Select: multiples canales sin saber cual va primeroemail1:=make(chan string)email2:=make(chan string)go message("mensaje 1", email1)go message("mensaje 2", email2)// tener presente la cantidad de channels a manejar para el ciclo, en este ejemplo 2fori:=0; i <2; i++{ select {casem1:=<-email1: fmt.Println("Email recibido de email 1", m1)casem2:=<-email2: fmt.Println("Email recibido de email 2", m2)}}
}
no entendi bien el select: ¿con qué criterio elige cual dato mostrar primero?
El select espera la llegada del mensaje en cualquiera de los canales. Acepta en este caso el que llegue primero. Si le agregas un sleep en la función message() puede ser que veas lo aleatorio que se vuelve quién llega primero.
Por qué si no cierras los _canales _no puedes leer los mensajes dentro del ciclo for?
Y lo contrario por qué falla en runtime si tratas de leer los mensajes dentro del ciclo sin cerrar los canales?
Porque cae en un loop infinito ya que el channel sigue estando atento al siguiente dato que se le mandará, la buena práctica es cerrarlo. Sino, la otra opción es con un ciclo for explícito indicando la cantidad de iteraciones como se ve en el minuto 8.
en la práctica para utilizaría un canal?
Los canales son el medio a través del cual las gorutinas pueden comunicarse entre sí. En términos simples, un canal es una tubería que permite que una gorutina coloque o lea los datos. Esto se puede usar para sincronizar procesos también que se comunicarían pasándose mensajes. Por ejemplo:
Ejercicio 1: Implementar una solución al problema de los lectores/escritores mediante paso asíncrono de mensajes.
Ejercicio 2: Implementar una solución al problema de los filósofos mediante paso asíncrono de mensajes.
Ejercicio 3: Una cuenta de ahorros es compartida por varias personas. Cada persona puede depositar o retirar una cantidad
positiva de dinero. El balance de la cuenta nunca puede ser negativo. Desarrollar un servidor que controle el acceso a la
cuenta mediante paso asíncrono de mensajes, así como el código de un cliente.
En un proyecto de mantos acuiferos distribuidos en una red multi conectada, no me interesa tener los datos secuenciales del flujo de presion y gasto que hay, si se puede, cada segundo.
Me gustaria poder usar coroutines para poder tener una medicion precisa y exacta de este dato, sin la necesidad de esperar a que la medicion en alguna otra locacion termine o concluya su ciclo.
En mi trabajo insertamos varias bases de datos y y tenemos que esperar a que se incerte una para seguir con la otra me parece que esto podria ayudar
Reto
No sé si sea verdad, pero se me hace que se utilizan en la compra de artículos en línea. Por ejemplo compras en cierto tienda en linea multinacional🛫🚚, y los artículos se encuentran en distintas partes del planeta🌎 y necesita coordinar🤓 cada uno de los paquetes de diferentes clientes, eso si es una locura🤯😵, bueno puede aplicarse. ¿Ustedes que opinan?
Creé algo rudimentario de Observador - Observable:
package main
import("fmt""time")func emisorA(output chan<- string){for{ output <-"Emisor: A" time.Sleep(time.Second*1)}}func emisorB(output chan<- string){for{ output <-"Emisor: b" time.Sleep(time.Second*3)}}func main(){canalA:=make(chan string)canalB:=make(chan string) go emisorA(canalA) go emisorB(canalB)for{ select {casecA:=<-canalA: fmt.Println(cA)casecB:=<-canalB: fmt.Println(cB)}}}
Los chanels con el parametro de cantidad se conocen como Buffered Channels verdad?
Exactamente, aquí un poco de información.
En el ejemplo con select ambos channels se cierran al finalizar el for? Si el programa siguiera necesitariamos cerrar esos dos channels?
El garbage collector de go se hace cargo de los channels que dejes abiertos durante la ejecución. Regularmente los cierras para indicar a los recivers que ya no se enviará más datos a través de ese canal.
En el minuto 4 lo que pasa es que el script queda en estado de deadlock porque hay un mensaje en espera y no hay ningún consumidor de mensajes. No da error por canal lleno.
fatal error: all goroutines are asleep - deadlock!
Al principio no entendía como funcionaba el channel y select, investigando un poco mas me doy cuenta de por que son necesarios para la concurrencia
Podrias explicarlo un poco mas a detalle por favor?, yo sigo sin entenderlo muy bien
Un proyecto de firma digital que use channels para manipular las diferentes solicitudes de firma de documentación en un banco-
Un servidor de chat es un ejemplo de donde se puede aprovechar la concurrencia de Go, ya que tienes miles de usuarios conectados, cada uno manejado por una goroutine que lee y escribe en un canal.
Hola, me gustaría saber la razón del porqué cuando se usa el for, nos dropea primero el del canal 2 antes que el del 1, suponiendo que el que registramos primero en el for y creamos primero fue el canal 1. ¿Existe una razón? Muchas gracias.
Esa parte del código los datos se envían concurrentemente, por lo que el orden de salida no necesariamente será el orden de entrada.