Cómo usar GoLang
Un LENGUAJE ASOMBROSO creado por Google en colaboración con Rob Pike, Ken Thomson y Robert Griesemer.
Ventajas:
- Rápido, compila directamente a código máquina sin necesidad de usar un intérprete.
- Fácil de aprender, muy buena documentación y muchas cosas simplificadas.
- Escala muy bien, soporta la programación concurrente a través de "GoRoutines".
- Recolector de basura automático, gestión automática de memoria.
- Motor de formateo incluido, no es necesario usar terceros.
- No se necesitan bibliotecas para pruebas o benchmarks porque ya están incluidas.
- Muy poco boilerplate para crear aplicaciones.
- Tiene una API para programación de redes, incluida como biblioteca estándar.
- MUY rápido, en algunas pruebas de rendimiento es más rápido que las aplicaciones backend hechas en Java y Rust.
- Sistema de plantillas incorporado, GENIAL para trabajar con HTMX.
Estructura Recomendada:
- ui (contenido relacionado con el frontend en caso de renderización en el servidor)
- html (plantillas)
- static (contenido estático multimedia y de estilo)
- assets
- css
- internal (contenido relacionado con herramientas y entidades reutilizables en todo el proyecto)
- models
- utils
- cmd
- web (contiene la lógica de la aplicación)
- domain (lógica de negocio)
- routes (rutas disponibles)
- web (contiene la lógica de la aplicación)
¿Cómo funciona GoLang?
GoLang utiliza un archivo base llamado go.mod, que contendrá el módulo principal que se llamará igual que el proyecto, y también la versión de Go utilizada. Luego, cada archivo tendrá la extensión ".go" para identificar que es un paquete perteneciente al lenguaje.
Pero... ¿qué es un paquete? Si vienes de JavaScript, puedes pensar en él de la misma manera que un módulo ES, ya que se usa para encapsular lógica relacionada. Pero a diferencia de los módulos ES, el paquete se identifica por las líneas de código "package packageName" en camel case el nombre del paquete en cuestión e importa la ubicación del paquete. Diferentes archivos que contienen lógica perteneciente al mismo paquete pueden organizarse por separado PERO deben estar bajo la misma carpeta principal, ya que esto es de suma importancia para luego importar dicho paquete en otros.
Para importar un paquete diferente se hace a través de la palabra "import" más la ruta a la que pertenece el paquete.
import "miProyecto/cmd/web/routes"
Si necesitas más de un paquete a la vez, no es necesario repetir la línea de código ya que se pueden agrupar usando "()" los diversos paquetes:
import (
"miProyecto/cmd/web/routes"
"miProyecto/internal/models"
)
Vale la pena mencionar que luego Go relacionará el nombre final de la ruta con el uso del paquete, por lo que para usar la lógica contenida en él se hará pensando en él como si fuera un objeto, donde cada propiedad representa un elemento lógico del paquete:
routes.MiRuta
Métodos de paquete privados y públicos:
Si el método comienza con minúscula, es un método privado, no se puede acceder desde fuera del paquete en sí mismo.
func miFuncion()
Si el método comienza con mayúscula, es un método público, se puede acceder importando el paquete desde otro.
func MiFuncion()
Ámbito del paquete
Veamos un archivo Go
package main - nombre del paquete
import "fmt" - paquete fmt importado, sin ruta porque es propio de Go
var numero int = 2
func main() {
i, j := 42, 2701 - variables locales al método, i con valor 42 y j con valor 2701
fmt.Println(i) - usando el método "Println" del paquete "fmt"
}
Seguro que has notado algo, "numero" tiene un tipo "int" precediendo la asignación de valor, mientras que "i" y "j" no lo tienen, esto se debe a que al igual que Typescript, Go infiere el tipo para esos primitivos. Veamos cómo trabajar con tipos.
Tipos de Datos
- bool = true / false
- string = cadena de caracteres
- int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr = valores numéricos enteros con sus límites, generalmente son 32 bits en sistemas de 32 bits y 64 para sistemas de 64 bits. Debería usarse entero a menos que haya una razón específica para usar un valor restringido.
- byte === uint8
- rune === int32
- float32, float64 = representa valores numéricos reales
- complex64, complex128 = números complejos que tienen una parte real y una parte imaginaria.
Structs
Representa una colección de propiedades, puedes pensar en ella como una interfaz de Typescript, ya que representa el contrato que debe seguir al crear una propiedad. Importante, si deseas que la propiedad sea accesible fuera del paquete, recuerda que debe comenzar con "mayúscula".
type Persona struct {
Nombre string
Apellido string
Edad int
}
var persona = Persona {
Nombre: "Caballero",
Apellido: "Programador",
Edad: 31
}
fmt.Println(persona.Nombre)
Otra forma:
var persona2 = Persona{"Caballero", "Programador", 31}
Arrays
Ahora comienza la diversión, los arrays son bastante diferentes de lo que estamos acostumbrados ya que DEBEN tener el número máximo de elementos que van a contener dentro:
var a [10]int - crea un array de 10 elementos de tipo int
a[0] = "Caballero"
O también
var a = [2]int{2, 3}
fmt.Println(a) - [2 3]
Si necesitamos que sea dinámico podemos hablar de "slices". Un "slice" es una porción de un array existente o una representación de una colección de elementos de un cierto tipo.
var primos = [6]int{2 ,3 ,5, 7, 11, 13}
var s []
int = primos[1:4] - crea un slice usando "primos" como base desde la posición 1 hasta la 4
fmt.Println(s) - [3 5 7]
s = append(s , 14)
fmt.Println(s) - [3 5 7 14]
fmt.Println(primos) - [2 3 5 7 14 13]
También puedes omitir valores para rangos máximo y mínimo haciéndolos tener valores predeterminados:
var a [10]int
es lo mismo que
a[0:10]
a[:10]
a[0:]
a[:]
Método Make
Para crear slices dinámicos puedes usar el método "make" incluido, esto creará un array lleno de elementos vacíos y devolverá un slice que se refiere a él. El método "len" se puede usar para ver cuántos elementos contiene actualmente y "cap" para ver su capacidad, es decir, cuántos elementos puede contener.
a := make([]int, 0, 5) // len(a)=0 cap(a)=5
Punteros
Si vienes de Javascript...
esto llevará un poco de tiempo, pero veamos juntos el siguiente ejemplo:
type TipoElemento struct {
nombre string
}
var ejemploElemento = TipoElemento {
nombre: "Caballero",
}
func MiFuncion(elemento TipoElemento) {
...
}
MiFuncion(ejemploElemento)
Aquí podrías pensar que estamos trabajando en el elemento "ejemploElemento", pero es todo lo contrario. Por defecto, Go creará una copia del elemento mismo para trabajar con él, por lo que el uso de "elemento" dentro de la función "MiFuncion" es diferente de "ejemploElemento"... es una copia.
Entonces, si queremos trabajar con el mismo elemento pasado como parámetro a la función, se debe usar un puntero. Normalmente, cuando creamos una variable, erróneamente decimos que creamos una propiedad que contiene un valor dentro de ella, cuando en realidad lo que estamos haciendo es crear un espacio de memoria que contiene un valor dentro de él, y luego creamos una referencia (puntero) a ese espacio de memoria que está representado por el nombre que damos a nuestra propiedad:
var a = 1
Crea un espacio de memoria que dentro contiene el valor "1" y creamos una referencia a ese espacio de memoria llamada "a". ¡La diferencia con Javascript es que esta referencia no se pasa al método a menos que hayamos creado un puntero a ella!
var p *int // puntero "p" que referenciará una propiedad de tipo "int"
i := 42
p = &i // crea un puntero directo a la propiedad "i"
// Si queremos acceder al valor referenciado por el puntero "p", usamos el nombre del puntero precedido por el símbolo "*"
fmt.Println(*p) // 42
*p = 21
fmt.Println(*p) // 21
Donde esto cambia es si apuntamos a una "estructura", ya que sería un poco engorroso hacer (*p).Propiedad, se reduce a usarlo como si fuera la estructura misma:
v := Persona{"Caballero"}
p := &v
p.Nombre = "Programador"
fmt.Println(v) // {Programador}
Valores Predeterminados
En Go, cuando declaras una variable sin asignar explícitamente un valor, toma un valor predeterminado basado en su tipo. Aquí tienes una tabla que resume los valores predeterminados:
Valores Predeterminados para Tipos de Datos:
- bool:
false
- string:
""
(cadena vacía) - Tipos Numéricos:
0
- array:
[0...]
(valores cero) - map:
nil
(no inicializado) - slice:
nil
(no inicializado) - puntero:
nil
(no inicializado) - función:
nil
(no inicializado)
Bucle Range
El bucle range
es una construcción poderosa para iterar sobre secuencias como slices, arrays, maps y strings. Proporciona dos componentes: el índice (i
) y el valor (v
) de cada elemento. Aquí tienes tres variaciones comunes:
- Iteración Completa:
var arr = []int{5, 4, 3, 2, 1}
for i, v := range arr {
fmt.Printf("índice: %d, valor: %d\n", i, v)
}
Este enfoque itera sobre tanto el índice como el valor de cada elemento en arr
.
- Ignorando Índice:
for _, v := range arr {
fmt.Printf("valor: %d\n", v)
}
El guion bajo (_
) descarta la información del índice, centrándose solo en los valores de los elementos.
- Ignorando Valor:
for i, _ := range arr {
fmt.Printf("índice: %d\n", i)
}
De manera similar, puedes usar un guion bajo para omitir el valor y acceder solo a los índices.
Maps
Los maps son colecciones desordenadas que asocian claves únicas (de cualquier tipo hashable) con valores. Go proporciona dos formas de crear y trabajar con maps:
-
Usando la Función
make
:type Persona struct { DNI, Nombre string } var m map[string]Persona func main() { m = make(map[string]Persona) m["123"] = Persona{"123", "pepe"} fmt.Println(m["123"]) }
-
Literal de Mapa:
type Persona struct { DNI, Nombre string } var m = map[string]Persona{ "123": Persona{"123", "pepe"}, "124": Persona{"124", "jorge"}, } func main() { fmt.Println(m) }
Los literales de mapas ofrecen una forma concisa de inicializar maps con pares clave-valor.
Mutando Maps
-
Inserción:
m[clave] = elemento
Agrega un nuevo par clave-valor al map
m
. -
Recuperación:
elemento = m[clave]
Funciones
Las funciones son bloques de código reutilizables que realizan t
areas específicas. Se declaran con la palabra clave func
, seguida del nombre de la función, la lista de parámetros (si los hay), el tipo de retorno (si lo hay) y el cuerpo de la función encerrado entre llaves.
Aquí tienes un ejemplo:
func saludar(nombre string) string {
return "¡Hola, " + nombre + "!"
}
func main() {
mensaje := saludar("Golang")
fmt.Println(mensaje)
}
Valores de Funciones
Las funciones pueden asignarse a variables, lo que te permite pasarlas como cualquier otro valor. Esto permite técnicas poderosas como las funciones de orden superior.
Aquí tienes un ejemplo que muestra cómo pasar una función como argumento y llamarla indirectamente:
func LlamarCallback(retroceso func(float64, float64) float64) float64 {
return retroceso(3, 4)
}
func hipotenusa(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
func main() {
fmt.Println(hipotenusa(5, 12))
fmt.Println(LlamarCallback(hipotenusa))
}
Closure
Los closures son un tipo especial de función que captura variables de su entorno circundante. Esto permite que el cierre acceda y manipule estas variables incluso después de que la función que lo rodea haya devuelto.
Aquí tienes un ejemplo de un cierre que crea una función "sumador" con una suma persistente:
func sumador() func(int) int {
suma := 0
return func(x int) int {
suma += x
return suma
}
}
func main() {
pos, neg := sumador(), sumador()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
Métodos
Go no tiene clases, pero permite definir métodos en tipos (structs, interfaces). Un método es una función asociada con un tipo, que toma un argumento receptor (generalmente el tipo mismo) que se refiere implícitamente al objeto sobre el que se llama el método.
Aquí tienes un ejemplo de una estructura Persona
con un método Saludar
:
type Persona struct {
Nombre, Apellido string
}
func (p Persona) Saludar() string {
return "Hola " + p.Nombre
}
func main() {
p := Persona{"Pepe", "Perez"}
fmt.Println(p.Saludar())
}
Los métodos también se pueden definir en tipos que no son estructuras:
type Nombre string
func (n Nombre) Saludar() string {
return "Hola " + string(n)
}
func main() {
nombre := Nombre("Pepe")
fmt.Println(nombre.Saludar())
}
Los métodos pueden aceptar punteros como receptores, lo que permite modificaciones al objeto original:
type Persona struct {
nombre, apellido string
}
func (p *Persona) cambiarNombre(n string) {
p.nombre = n
}
func main() {
p := Persona{"pepe", "perez"}
p.cambiarNombre("juan")
fmt.Println(p) // Salida: {juan perez}
pp := &Persona{"puntero", "persona"}
pp.cambiarNombre("punteroNuevoNombre")
fmt.Println(*pp) // Salida: {punteroNuevoNombre persona}
}
Go automáticamente desreferencia receptores de puntero cuando es necesario, por lo que no siempre necesitas usar el operador *
explícitamente.
Interfaces
Las interfaces definen un conjunto de métodos que un tipo debe implementar. Proporcionan una manera de lograr polimorfismo, permitiendo que diferentes tipos se usen de manera intercambiable siempre que implementen los métodos requeridos.
Aquí tienes un ejemplo de una Interfaz
que define dos métodos, Saludar
y Moverse
:
type Persona interface {
Saludar() string
Moverse() string
}
type Alumno struct {
Nombre string
}
func (a Alumno) Saludar() string {
return "Hola " + a.Nombre
}
func (a Alumno) Moverse() string {
return "Estoy caminando"
}
func main() {
var persona Persona = Alumno{
"Pepe",
}
fmt.Println(persona.Saludar())
fmt.Println(persona.Moverse())
}
Valores de Interfaces con Nil
Los valores de las interfaces pueden ser nil
, lo que indica que no tienen una referencia a ningún objeto específico. Aquí tienes un ejemplo que muestra cómo manejar valores de interfaces nil
:
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
var t *T
i = t
describe(i)
i.M() // Salida: <nil>
i = &T{"hola"}
describe(i)
i.M() // Salida: hola
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
Interfaces Vacías
Si no conoces los métodos específicos que una interfaz podría requerir de antemano, puedes crear una interfaz vacía usando el tipo interface{}
. Esto te permite almacenar cualquier valor en la interfaz, pero no podrás llamar métodos directamente sobre él.
var i interface{}
Aserción de tipo
Cuando usamos una interfaz vacía interface{}
, podemos utilizar cualquier tipo de dato, PERO, esto también viene con problemas. ¿Cómo sabemos si el parámetro de un método es del tipo esperado si es una interfaz vacía? Aquí es donde las Aserciones de Tipo resultan útiles, ya que proporcionan la posibilidad de probar si la interfaz vacía es del tipo esperado.
t := i.(T)
Esto significa que el valor de la interfaz i
tiene el tipo concreto T
y asigna el valor subyacente T
a la variable t
.
Si i
no tiene un tipo T
, esto provocará un error en tiempo de ejecución (panic).
Puedes probar si la interfaz contiene un tipo específico usando un segundo parámetro, al igual que lo hacemos con err
:
t, ok := i.(T)
Esto guardará true
o false
dentro de ok
. Si es false
, t
guardará un valor cero y no ocurrirá ningún error en tiempo de ejecución.
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s) // hello
s, ok := i.(string)
fmt.Println(s, ok) // hello true
f, ok := i.(float64)
fmt.Println(f, ok) // 0 false
f = i.(float64) // panic: conversión de interfaz: la interfaz {} es string, no float64
fmt.Println(f) // nada, generará un error antes
}
Switch de tipo
Proporciona la posibilidad de hacer más de una aserción de tipo en serie.
Al igual que un switch regular, pero usamos tipos en lugar de valores, y los últimos se compararán con el tipo del valor contenido por la interfaz dada.
switch v := i.(type) {
case T:
// si v tiene tipo T
case S:
// si v tiene tipo S
default:
// si v no tiene ni tipo T ni tipo S, tendrá el mismo tipo que "i"
}
Aclaración: al igual que las Aserciones de Tipo, usamos un tipo como parámetro i.(T)
, pero en lugar de usar T
, necesitamos usar la palabra clave type
.
Esto es excelente cuando ejecutamos lógicas diferentes que dependen del tipo del parámetro:
type Saludador interface {
Saludar()
}
type Persona struct {
Nombre string
}
func (p Persona) Saludar() {
fmt.Printf("¡Hola, mi nombre es %s!\n", p.Nombre)
}
type Numero int
func (n Numero) Saludar() {
if n%2 == 0 {
fmt.Printf("¡Hola, soy un número par: %d!\n", n)
} else {
fmt.Printf("¡Hola, soy un número impar: %d!\n", n)
}
}
func main() {
saludadores := []Saludador{
Persona{"Alice"},
Persona{"Bob"},
Numero(3),
Numero(4),
}
for _, saludador := range saludadores {
switch valor := saludador.(type) {
case Persona:
valor.Saludar()
case Numero:
valor.Saludar()
}
}
}
Stringers
Es un tipo que se define a sí mismo como string
, está definido por el paquete fmt
y se utiliza para imprimir valores.
type Persona struct {
Nombre string
Edad int
}
func (p Persona) String() string {
return fmt.Sprintf("%v (%% años)", p.Nombre, p.Edad)
}
func main() {
a := Persona{"Gentleman Programming", 32}
z := Persona{"Alan Buscaglia", 32}
fmt.Println(a, z) // Gentleman Programming (32 años) Alan Buscaglia (32 años)
}
Ejemplo usando Stringers para modificar la forma en que mostramos una dirección IP al usar fmt.Println :
type IPAddr [4]byte
func (ip IPAddr) String() string {
str := ""
for i, ipValor := range ip {
str += fmt.Sprint(ipValor)
if i < len(ip)-1 {
str += "."
}
}
return fmt.Sprintf("%s", str)
}
// TODO: Agregar un método "String() string" a IPAddr.
func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for _, ip := range hosts {
fmt.Println(ip)
}
}
Errores
Para mostrar errores, Go utiliza valores de error
para expresar estados de error
, y para esto, existe el tipo error
y es similar a la interfaz fmt.Stringer
:
type error interface {
Error() string
}
Exactamente como con fmt.Stringer
, el paquete fmt
busca la interfaz error
al imprimir valores. Normalmente, los métodos devuelven un valor de error
y deberíamos usarlo para gestionar qué hacer en caso de que sea diferente a nil
:
i, err := strconv.Atoi("42")
if err != nil {
fmt.Println("no se pudo convertir el número: %v\n", err)
return
}
fmt.Println("Entero convertido: ", i)
Lectores
Otra gran interfaz que representa el extremo de lectura de un flujo de datos, estos datos pueden ser transmitidos a través de archivos, conexiones de red, compresores, cifrados, etc.
Y tiene un método Read
:
func (T) Read(b []byte) (n int, err error)
Este método poblará el array de bytes con datos y devolverá el número de bytes poblados y un valor de error. Devuelve un error de io.EOF
cuando termina el flujo.
func main() {
data := "Gentleman Programming"
// crea un nuevo io.Reader leyendo desde data
lector := strings.NewReader(data)
// crea un buffer para almacenar los datos copiados
var buffer strings.Builder
// copia los datos del lector a un buffer. io.Copy lee del lector y escribe en el escritor hasta que se alcanza EOF en el lector o ocurre un error
n, err := io.Copy(&buffer, lector)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("\n%d bytes copiados exitosamente. \n", n)
// accede a los datos copiados en el buffer
fmt.Println("Datos copiados:", buffer.String())
}
}
Ejemplo, ¡obtengamos una cadena cifrada y descifrémosla!
package main
import (
"io"
"os"
"strings"
)
type rot13Reader struct {
r io.Reader
}
func (rr *rot13Reader) Read(p []byte) (n int, err error) {
n, err = rr.r.Read(p)
for i := 0; i < n; i++ {
if (p[i] >= 'A' && p[i] <= 'Z') || (p[i] >= 'a' && p[i] <= 'z') {
if p[i] <= 'Z' {
// p[i] - 'A' calcula la posición del carácter actual en relación con 'A', luego agregamos 13 ya que el algoritmo ROT13 desplaza cada letra en el alfabeto 13 posiciones,
// luego aplicamos '%26' para asegurarnos de que el resultado esté dentro del rango del alfabeto (26 letras)
// y al final agregamos 'A' que convierte el resultado nuevamente al valor ASCII de una letra
p[i] = (p[i]-'A'+13)%26 + 'A'
} else {
p[i] = (p[i]-'a'+13)%26 + 'a'
}
}
}
return
}
func main() {
s := strings.NewReader("Lbh penpxrq gur pbqr!")
r := rot13Reader{s}
io.Copy(os.Stdout, &r)
}
Imágenes
El paquete images
define la interfaz Image
, que es una herramienta poderosa para trabajar con imágenes ya que puedes crear, manipular y decodificar varios tipos de imágenes como PNG, JPEG, GIF, BMP, y más:
package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
Creemos una imagen pequeña y negra con un píxel rojo en el centro:
package main
import (
"image"
"image/color"
"image/png"
"os"
)
func main() {
// Crea una nueva imagen RGBA con dimensiones 100x100
img := image.NewRGBA(image.Rect(0, 0, 100, 100))
// Establece todos los píxeles en negro
for x := 0; x < 100; x++ {
for y := 0; y < 100; y++ {
img.Set(x, y, color.Black)
}
}
// Establece el píxel en el centro en rojo
img.Set(50, 50, color.RGBA{255, 0, 0, 255})
// Crea un archivo PNG para guardar la imagen
archivo, err := os.Create("imagen_simple.png")
if err != nil {
panic(err)
}
defer archivo.Close()
// Codifica la imagen al formato PNG y guárdala en el archivo
err = png.Encode(archivo, img)
if err != nil {
panic(err)
}
println("¡Imagen simple generada exitosamente!")
}
GoRoutines
Como mencionamos anteriormente, Go es un lenguaje que admite la programación concurrente a través de "GoRoutines". Una GoRoutine es un hilo ligero gestionado por el tiempo de ejecución de Go, lo que te permite ejecutar múltiples funciones concurrentemente.
PERO es diferente a otros lenguajes, ya que es un hilo virtual que se ejecuta en un hilo real y es gestionado por el tiempo de ejecución de Go.
Para ejecutar una función como GoRoutine, solo necesitas agregar la palabra clave go
antes de la llamada a la función:
go f(x, y, z)
Esto ejecutará la función f(x, y, z)
concurrentemente en una nueva GoRoutine. Los parámetros se evalúan en el momento de la llamada a la función, por lo que si cambian más tarde, la GoRoutine usará los valores actualizados.
Veamos un ejemplo:
package main
import (
"fmt"
)
func say(s string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
}
}
func main() {
// Lanza una nueva goroutine para ejecutar la función say con "Hello"
go say("Hello")
// Imprime "World" 3 veces en la función principal
for i := 0; i < 3; i++ {
fmt.Println("Gentleman")
}
}
Cuando ejecutas este código, la salida no será necesariamente "Hello" seguido de "Gentleman" tres veces cada uno. Esto se debe a que las goroutines se ejecutan concurrentemente. Podrías ver "Hello" y "Gentleman" mezclados.
Las goroutines son ligeras, por lo que puedes crear miles de ellas sin ningún problema de rendimiento. Son gestionadas por el tiempo de ejecución de Go, que las programa eficientemente en hilos reales del sistema operativo.
Otra gran característica es que se ejecutan en el mismo espacio de direcciones, por lo que pueden comunicarse entre sí utilizando canales para compartir memoria, pero esto también debe ser gestionado y sincronizado.
Para hacerlo, podemos usar los canales
:
Canales
Serán nuestra forma de comunicarnos entre goroutines, son tipados y se pueden utilizar para enviar y recibir datos con el operador de canal <-
:
ch <- v // Envía v al canal ch.
v := <-ch // Recibe de ch y asigna el valor a v.
Los datos fluirán en la dirección de la flecha, por lo que si quieres enviar datos a un canal, debes usar la flecha que apunta al canal, y si quieres recibir datos de un canal, debes usar la flecha que apunta desde el canal.
También puedes crear un canal con la función make
:
ch := make(chan int)
Esto creará un canal que enviará y recibirá enteros.
Por defecto, los envíos y recepciones bloquean hasta que el otro lado esté listo. Esto permite que las goroutines se sincronicen sin que tengamos que gestionar manualmente esa sincronización.
package main
import (
"fmt"
)
func say(s string, ch chan string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
ch <- s // Envía "Hello" al canal después de cada impresión
}
}
func main() {
// Crea un canal para almacenar cadenas
ch := make(chan string)
// Lanza una nueva goroutine para ejecutar la función say
go say("Hello", ch)
// Espera infinitamente mensajes en el canal (asegura que se impriman todos los "Hello")
for {
msg := <-ch // Recibe mensaje del canal
fmt.Println("Received:", msg)
}
fmt.Println("Gentleman") // Imprime "Gentleman" después de recibir todos los mensajes
}
Canales con Búfer
Todos los canales pueden tener un búfer, esto significa que pueden contener un número limitado de valores sin que haya un receptor correspondiente para esos valores.
Cuando el canal está lleno, el remitente se bloqueará hasta que el receptor haya recibido un valor. Esto es extremadamente útil cuando quieres enviar múltiples valores y no quieres perderlos si el receptor no está listo.
ch := make(chan int, 100)
Esto creará un canal que puede contener hasta 100 enteros.
Si envías más de 100 valores al canal, el remitente se bloqueará hasta que el receptor haya recibido algunos valores.
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3 // error fatal: todas las goroutines están dormidas - ¡deadlock!
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Range y Close
¡Puedes cerrar un canal en cualquier momento! Un momento recomendado para cerrar un canal es cuando deseas señalar que no se enviarán más valores en él y quien debe hacerlo debería ser el remitente, nunca el receptor, ya que enviar en un canal cerrado causará un pánico:
v, ok := <-ch
ok será false si no hay más valores para recibir y el canal está cerrado.
Para recibir valores de un canal hasta que se cierre, puedes usar range
:
for i:= range ch {
fmt.Println(i)
}
¿Necesitamos cerrarlos? No necesariamente, solo si el receptor necesita saber que no se enviarán más valores, o si el remitente necesita decirle al receptor que ha terminado de enviar valores, de esta manera terminaremos el bucle range
.
Ejemplo:
func say(s string, ch chan string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
ch <- s // Envía "Hello" al canal después de cada impresión
}
close(ch) // Cierra el canal después de enviar los mensajes
}
func main() {
// Crea un canal para almacenar cadenas
ch := make(chan string)
// Lanza una nueva goroutine para ejecutar la función say
go say("Hello", ch)
// Bucle para recibir e imprimir mensajes hasta que el canal se cierre
for {
msg, ok := <-ch // Recibe el mensaje y verifica el estado abierto del canal
if !ok {
break // Salir del bucle si el canal está cerrado
}
fmt.Println("Received:", msg)
}
fmt.Println("Mensajes recibidos. Saliendo.")
}
Selección de GoRoutines
La declaración select
permite que una goroutine espere en múltiples operaciones de comunicación. Se bloquea hasta que uno de sus casos pueda ejecutarse, luego ejecuta ese caso.
Es útil cuando deseas esperar en múltiples canales y realizar acciones diferentes según qué canal esté listo.
func say(s string, ch chan string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
ch <- s // Envía "Hello" al canal después de cada impresión
}
// Cierra el canal después de que el bucle termine de enviar mensajes
close(ch)
}
func main() {
// Crea un canal para almacenar cadenas
ch := make(chan string)
// Lanza una nueva goroutine para ejecutar la función say
go say("Hello", ch)
// Usa select para manejar mensajes desde el canal o un tiempo de espera
for {
select {
case msg, ok := <-ch: // Recibe el mensaje y verifica el estado abierto del canal
if !ok {
fmt.Println("Canal cerrado. Saliendo.")
break
}
fmt.Println("Received:", msg)
case <-time.After(1 * time.Second): // Expira después de 1 segundo si no se recibe ningún mensaje
fmt.Println("Tiempo de espera esperando mensaje.")
break
}
}
}
También puedes usar un caso default
en una declaración select
, esto se ejecutará si ningún otro caso está listo:
func say(s string, ch chan string) {
for i := 0; i < 3; i++ {
fmt.Println(s)
ch <- s // Envía "Hello" al canal después de cada impresión
}
close(ch) // Cierra el canal después de enviar mensajes
}
func main() {
// Crea un canal para almacenar cadenas
ch := make(chan string)
// Lanza una nueva goroutine para ejecutar la función say
go say("Hello", ch)
// Usa select con un caso predeterminado
for {
select {
case msg, ok := <-ch:
if !ok {
fmt.Println("Canal cerrado. Saliendo.")
break
}
fmt.Println("Received:", msg)
case <-time.After(1 * time.Second):
fmt.Println("Tiempo de espera esperando mensaje.")
break
default:
fmt.Println("Nada para recibir o aún no se ha agotado el tiempo de espera.")
}
}
}
Ahora hagamos un ejercicio donde comprobaremos si dos árboles de nodos tienen la misma secuencia de valores:
package main
import (
"fmt"
)
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func isSameSequence(root1, root2 *TreeNode) bool {
seq1 := make(map[int]bool)
seq2 := make(map[int]bool)
traverse(root1, seq1)
traverse(root2, seq2)
return equal(seq1, seq2)
}
func traverse(node *TreeNode, seq map[int]bool) {
if node == nil {
return
}
seq[node.Val] = true
traverse(node.Left, seq)
traverse(node.Right, seq)
}
func equal(seq1, seq2 map[int]bool) bool {
if len(seq1) != len(seq2) {
return false
}
for val := range seq1 {
if !seq2[val] {
return false
}
}
return true
}
func main() {
// Construyendo el primer árbol binario
root1 := &TreeNode{
Val: 3,
Left: &TreeNode{
Val: 1,
Left: &TreeNode{
Val: 1,
},
Right: &TreeNode{
Val: 2,
},
},
Right: &TreeNode{
Val: 8,
Left: &TreeNode{
Val: 5,
},
Right: &TreeNode{
Val: 13,
},
},
}
// Construyendo el segundo árbol binario
root2 := &TreeNode{
Val: 8,
Left: &TreeNode{
Val: 3,
Left: &TreeNode{
Val: 1,
Left: &TreeNode{
Val: 1,
},
Right: &TreeNode{
Val: 2,
},
},
Right: &TreeNode{
Val: 5,
},
},
Right: &TreeNode{
Val: 13,
},
}
fmt.Println(isSameSequence(root1, root2)) // Salida: true
}
Mutex
Algo de lo que debemos cuidarnos al trabajar con GoRoutines es el acceso a la memoria compartida, donde más de una GoRoutine puede acceder al mismo espacio de memoria al mismo tiempo, lo que puede provocar grandes conflictos.
Este concepto se llama exclusión mutua
, y se resuelve mediante el uso de mutexes
, que se utilizan para sincronizar el acceso a la memoria compartida.
import ("sync")
var mu sync.Mutex
Tiene dos métodos, Lock
y Unlock
, que se utilizan para proteger la memoria compartida:
func safeIncrement() {
mu.Lock() // bloquea la memoria compartida
defer mu.Unlock() // desbloquea la memoria compartida cuando la función retorna
count++ // incrementa la memoria compartida
}
Aquí estamos utilizando la declaración defer
para asegurar que el mutex se desbloquee cuando la función retorne, incluso si entra en pánico.
package main
import (
"fmt"
"sync"
)
type TreeNode struct {
Left *TreeNode
Right *TreeNode
Val int
}
type SequenceCollector struct {
sequence map[int]bool
mutex sync.Mutex
}
func isSameSequence(root1, root2 *TreeNode) bool {
seq1 := &SequenceCollector{sequence: make(map[int]bool)}
seq2 := &SequenceCollector{sequence: make(map[int]bool)}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
traverse(root1, seq1)
}()
go func() {
defer wg.Done()
traverse(root2, seq2)
}()
return equal(seq1.sequence, seq2.sequence)
}
func traverse(node *TreeNode, seq *SequenceCollector) {
if node == nil {
return
}
seq.mutex.Lock()
seq.sequence[node.Val] = true
seq.mutex.Unlock()
traverse(node.Left, seq)
traverse(node.Right, seq)
}
func equal(seq1, seq2 map[int]bool) bool {
if len(seq1) != len(seq2) {
return false
}
for val := range seq1 {
if !seq2[val] {
return false
}
}
return true
}
func main() {
root1 := &TreeNode{
Val: 3,
Left: &TreeNode{
Val: 1,
Left: &TreeNode{
Val: 1,
},
Right: &TreeNode{
Val: 2,
},
},
Right: &TreeNode{
Val: 8,
Left: &TreeNode{
Val: 5,
},
Right: &TreeNode{
Val: 13,
},
},
}
root2 := &TreeNode{
Val: 8,
Left: &TreeNode{
Val: 3,
Left: &TreeNode{
Val: 1,
Left: &TreeNode{
Val: 1,
},
Right: &TreeNode{
Val: 2,
},
},
Right: &TreeNode{
Val: 5,
},
},
Right: &TreeNode{
Val: 13,
},
}
fmt.Println(isSameSequence(root1, root2)) // true
}