Translate

lunes, 2 de marzo de 2026

Canales tipados en Go: comunicación segura entre goroutines


Una de las ideas más elegantes de Golang es su modelo de concurrencia por comunicación.

En lugar de compartir memoria y protegerla con locks, Go propone lo contrario: no comuniques compartiendo memoria; compartí memoria comunicándote.

La pieza central de este modelo son los canales — estructuras tipadas que permiten pasar valores entre goroutines (hilos livianos de ejecución) de manera segura y sincronizada.

Un canal (chan) es una estructura que conecta dos goroutines, permitiendo enviar y recibir valores.

Podés imaginarlo como un tubo: lo que se envía en un extremo se recibe en el otro.


ch := make(chan int) // canal que transporta enteros


go func() {

    ch <- 42 // enviar

}()


valor := <-ch // recibir

fmt.Println(valor) // imprime 42


Los canales son tipados: chan int solo puede transportar int, chan string solo string, y así sucesivamente.

Esto significa que el compilador verifica que el tipo del valor enviado o recibido sea correcto.

Una característica clave es que el envío y la recepción están sincronizados:

  • Si una goroutine intenta enviar en un canal sin que otra esté lista para recibir, queda bloqueada.
  • Lo mismo ocurre a la inversa: recibir de un canal vacío bloquea la goroutine hasta que alguien envíe.

Esto elimina la necesidad de usar mutexes o locks para coordinar acceso compartido: los canales actúan como puntos de sincronización naturales.


func worker(done chan bool) {

    fmt.Println("Trabajo iniciado...")

    time.Sleep(time.Second)

    fmt.Println("Trabajo finalizado.")

    done <- true

}


func main() {

    done := make(chan bool)

    go worker(done)

    <-done // espera hasta que worker termine

}


Por defecto, los canales son sin buffer (bloqueantes).

Pero podés crear canales con buffer especificando su capacidad:


ch := make(chan string, 2)

ch <- "hola"

ch <- "mundo"

// No se bloquea hasta llenar el buffer


fmt.Println(<-ch)

fmt.Println(<-ch)


En este caso, el envío solo bloquea si el buffer está lleno, y la recepción solo bloquea si está vacío.

Esto permite desacoplar parcialmente la velocidad de producción y consumo, como en una cola de mensajes ligera.


Otra propiedad importante (y poco conocida por los principiantes) es que los canales pueden ser direccionales.

Podés declarar que una función solo envía o solo recibe valores, reforzando la seguridad de tipos.


func productor(ch chan<- int) {

    for i := 1; i <= 3; i++ {

        ch <- i

    }

    close(ch)

}


func consumidor(ch <-chan int) {

    for n := range ch {

        fmt.Println("Recibido:", n)

    }

}


func main() {

    ch := make(chan int)

    go productor(ch)

    consumidor(ch)

}


Esto evita errores donde una goroutine accidentalmente intente usar un canal en la dirección incorrecta.

En otras palabras, los canales no solo tipan qué pasa, sino también cómo pasa.


Cuando ya no vas a enviar más datos, podés cerrar el canal con close(ch).

El receptor puede detectar el cierre de dos formas:

  1. Usando for range, que itera hasta que el canal se cierra.
  2. Usando la forma de recepción doble:



valor, ok := <-ch

if !ok {

    fmt.Println("Canal cerrado")

}


Cerrar un canal indica “no habrá más valores”.

Intentar enviar a un canal cerrado provoca panic, lo que fuerza a un manejo explícito del ciclo de vida del canal.


El select de Go permite esperar simultáneamente por varios canales, y ejecutar la rama que esté lista primero.


ch1 := make(chan string)

ch2 := make(chan string)


go func() {

    time.Sleep(time.Second)

    ch1 <- "uno"

}()

go func() {

    time.Sleep(2 * time.Second)

    ch2 <- "dos"

}()


for i := 0; i < 2; i++ {

    select {

    case msg1 := <-ch1:

        fmt.Println("Recibido de ch1:", msg1)

    case msg2 := <-ch2:

        fmt.Println("Recibido de ch2:", msg2)

    }

}


select convierte la concurrencia en Go en algo composable: múltiples fuentes de eventos, sin bloqueos ni polling activo.


Patrones comunes con canales:

  1. Fan-out / Fan-in: múltiples productores envían al mismo canal (fan-in) o un productor reparte el trabajo entre varios canales (fan-out).
  2. Pipeline: salida de una goroutine es entrada de otra, formando cadenas de procesamiento asíncronas.
  3. Worker pools: un conjunto de goroutines consumen tareas de un canal común y publican resultados en otro.


Estos patrones se logran con pocas líneas y sin necesidad de bibliotecas adicionales.


Mientras que en lenguajes como Java o C# el modelo concurrente se basa en bloqueos y memoria compartida, Go opta por un modelo más cercano al de CSP (Communicating Sequential Processes):

cada proceso (goroutine) se comunica solo mediante mensajes.

En Go, el canal tipado reemplaza estructuras de sincronización manual como colas bloqueantes o mutexes.

En Erlang, los procesos se comunican con mensajes asíncronos no tipados.

Go adopta una versión más estricta y estáticamente verificada: los canales tienen tipo, dirección y límites de buffer conocidos.


Esto hace que la concurrencia sea más predecible y verificable en tiempo de compilación, reduciendo el riesgo de errores de sincronización y data races.


domingo, 1 de marzo de 2026

Reloj digital en elm


Hasta ahora nos hemos centrado en los comandos. Con los ejemplos de HTTP y aleatoriedad, le ordenamos a Elm que realizara una tarea específica inmediatamente, pero ese es un patrón un tanto extraño para un reloj. Siempre queremos saber la hora actual. ¡Aquí es donde entran en juego las suscripciones!


import Browser

import Html exposing (..)

import Task

import Time




-- MAIN



main =

  Browser.element

    { init = init

    , view = view

    , update = update

    , subscriptions = subscriptions

    }




-- MODEL



type alias Model =

  { zone : Time.Zone

  , time : Time.Posix

  }



init : () -> (Model, Cmd Msg)

init _ =

  ( Model Time.utc (Time.millisToPosix 0)

  , Task.perform AdjustTimeZone Time.here

  )




-- UPDATE



type Msg

  = Tick Time.Posix

  | AdjustTimeZone Time.Zone




update : Msg -> Model -> (Model, Cmd Msg)

update msg model =

  case msg of

    Tick newTime ->

      ( { model | time = newTime }

      , Cmd.none

      )


    AdjustTimeZone newZone ->

      ( { model | zone = newZone }

      , Cmd.none

      )




-- SUBSCRIPTIONS



subscriptions : Model -> Sub Msg

subscriptions model =

  Time.every 1000 Tick




-- VIEW



view : Model -> Html Msg

view model =

  let

    hour   = String.fromInt (Time.toHour   model.zone model.time)

    minute = String.fromInt (Time.toMinute model.zone model.time)

    second = String.fromInt (Time.toSecond model.zone model.time)

  in

  h1 [] [ text (hour ++ ":" ++ minute ++ ":" ++ second) ]


Todo lo nuevo proviene del paquete elm/time. 


Para trabajar con el tiempo correctamente en programación, necesitamos tres conceptos diferentes:

Hora Humana: Es lo que se ve en los relojes (8 a. m.) o en los calendarios (3 de mayo). ¡Genial! Pero si mi llamada es a las 8 a. m. en Boston, ¿qué hora es para mi amigo en Vancouver? Si es a las 8 a. m. en Tokio, ¿es el mismo día en Nueva York? (¡No!) Por lo tanto, entre las zonas horarias basadas en fronteras políticas en constante cambio y el uso inconsistente del horario de verano, ¡la hora humana básicamente nunca debería almacenarse en el modelo ni en la base de datos! ¡Es solo para visualización!

POSIX Time: Con POSIX Time, no importa dónde vivas ni en qué época del año sea. Es simplemente la cantidad de segundos transcurridos desde un momento arbitrario (en 1970). Donde quiera que vayas en la Tierra, la hora POSIX es la misma.

Zonas Horarias: Una "zona horaria" es un conjunto de datos que te permite convertir la hora POSIX en hora humana. ¡Pero no se trata solo de UTC-7 o UTC+3! Las zonas horarias son mucho más complicadas que una simple diferencia horaria. Cada vez que Florida cambia al horario de verano para siempre o Samoa cambia de UTC-11 a UTC+13, alguien añade una nota a la base de datos de zonas horarias de la IANA. Esa base de datos se carga en cada ordenador, y entre la hora POSIX y todos los casos excepcionales de la base de datos, ¡podemos calcular la hora humana!


Así que, para mostrarle la hora a un ser humano, siempre debes conocer Time.Posix y Time.Zone. ¡Eso es todo! Así que todo lo relacionado con la "hora humana" es para la función de vista, no para el modelo. De hecho, puedes verlo en nuestra vista:


vista: Modelo -> Mensaje HTML

vista modelo =

let

hora = String.fromInt (Time.toHour modelo.zone modelo.time)

minuto = String.fromInt (Time.toMinute modelo.zone modelo.time)

segundo = String.fromInt (Time.toSecond modelo.zone modelo.time)

in

h1 [] [texto (hora ++ ":" ++ minuto ++ ":" ++ segundo) ]


La función Time.toHour toma Time.Zone y Time.Posix nos devuelve un entero de 0 a 23 que indica qué hora es en tu zona horaria.


¿Cómo obtenemos nuestro Time.Posix? ¡Con una suscripción! Suscripciones: Modelo -> Submensaje


subscriptions : Model -> Sub Msg

subscriptions model =

  Time.every 1000 Tick


Usamos la función Time.every:


every : Float -> (Time.Posix -> msg) -> Sub msg


Requiere dos argumentos:

  • Un intervalo de tiempo en milisegundos. Dijimos 1000, que significa cada segundo. Pero también podríamos decir 60 * 1000 por cada minuto, o 5 * 60 * 1000 por cada cinco minutos.
  • Una función que convierte la hora actual en un mensaje. Por lo tanto, cada segundo, la hora actual se convertirá en un Tick <time> para nuestra función de actualización.

Ese es el patrón básico de cualquier suscripción. Se proporciona cierta configuración y se describe cómo generar valores de mensaje. 

Obtener Time.Zone es un poco más complicado. Nuestro programa creó un comando con:


Task.perform AdjustTimeZone Time.here


Leer la documentación de la tarea es la mejor manera de comprender esta línea. La documentación está escrita para explicar los nuevos conceptos, y creo que sería una digresión excesiva incluir una versión peor de esa información aquí. La cuestión es simplemente que ordenamos al entorno de ejecución que nos proporcione la Time.Zone dondequiera que se ejecute el código.

viernes, 27 de febrero de 2026

Ownership y préstamos en Rust


Rust introduce dos ideas que, juntas, son su rasgo distintivo: ownership (propiedad) y borrowing (préstamos).

No son trucos sintácticos: son un modelo de memoria que permite seguridad en tiempo de compilación (sin garbage collector) y rendimiento predecible.

Ownership: cada valor en Rust tiene un dueño (variable). Cuando el dueño sale de scope, el valor se libera.

Move: asignar o pasar por valor mueve la propiedad por defecto (para tipos no Copy).

Borrow / préstamo: podés tomar prestado un valor como referencia inmutable &T o mutable &mut T.

Reglas clave: puedes tener muchas referencias inmutables simultáneas, o una referencia mutable — nunca ambas.

Lifetimes: anotaciones que garantizan que las referencias no apunten a memoria liberada.


En Rust cada valor tiene exactamente un dueño. Cuando el dueño deja de existir (sale del scope), Rust llama drop automáticamente.


fn main() {

    let s1 = String::from("hola"); // s1 es dueño del heap con "hola"

    let s2 = s1; // move: s1 ya no es válido

    // println!("{}", s1); // ERROR: s1 fue movido a s2

    println!("{}", s2); // OK

}


Para tipos que implementan el trait Copy (primitivos como i32, bool), la asignación copia el valor en vez de moverlo.


Si querés duplicar un String, podés clonar explícitamente:


let s1 = String::from("hola");

let s2 = s1.clone(); // copia profunda

println!("{}", s1); // OK

println!("{}", s2); // OK


Mentalidad: en Rust la transferencia de propiedad es explícita — evita copias accidentales que degradan rendimiento.


En lugar de mover la propiedad, podés prestar acceso usando referencias:


fn longitud(s: &String) -> usize {

    s.len()

}


fn main() {

    let s = String::from("hola");

    let len = longitud(&s); // prestamos &s, s sigue siendo dueño

    println!("La longitud es {}", len);

}


Para modificar lo prestado, necesitás referencia mutable:


fn agregar_excl(s: &mut String) {

    s.push('!');

}


fn main() {

    let mut s = String::from("hola");

    agregar_excl(&mut s);

    println!("{}", s); // "hola!"

}


Rust impone reglas estrictas (en tiempo de compilación):

  1. Puedes tener cero o más referencias inmutables (&T) simultáneas.
  2. O puedes tener una sola referencia mutable (&mut T) — y ninguna inmutable simultánea.
  3. Las referencias deben ser válidas mientras se usan (lifetime).

Estas reglas evitan condiciones de carrera y referencias colgantes sin costo en tiempo de ejecución.


Ejemplo de error típico: aliasing mutable


fn main() {

    let mut s = String::from("hola");

    let r1 = &s;       // préstamo inmutable

    let r2 = &s;       // otro préstamo inmutable

    // let r3 = &mut s; // ERROR: no se puede pedir &mut mientras existan &


    println!("{}, {}", r1, r2);

}


Si intentás mezclar &T y &mut T en solapamiento, el compilador falla. Para arreglarlo, reorganizá el scope para que las referencias inmutables dejen de usarse antes de pedir la mutable.

Rust no permite que una referencia viva más que el valor apuntado. Los lifetimes (habitualmente inferidos) son la forma del compilador de garantizarlo.


Ejemplo que no compila:


fn devuelve_referencia() -> &String {

    let s = String::from("hola");

    &s // ERROR: s se libera al salir del scope

}


Corrección: la referencia debe apuntar a algo que viva lo suficiente (por ejemplo, una referencia pasada desde afuera), o devolver el valor moviéndolo en vez de devolver una referencia.


Con funciones que reciben y devuelven referencias a datos externos, a veces necesitás anotar lifetimes explícitamente:


fn mayor<'a>(x: &'a str, y: &'a str) -> &'a str {

    if x.len() >= y.len() { x } else { y }

}


Aquí a declara que la referencia retornada vivirá al menos tanto como las referencias x y y. Las anotaciones ayudan al compilador a razonar sobre efectos temporales; muchas veces Rust infiere las anotaciones por vos.


El borrow checker es el mecanismo que verifica las reglas de préstamos y lifetimes en compilación.

Sus efectos:

  • Evita use after free y data races en concurrencia sin coste en tiempo de ejecución.
  • Obliga a estructurar el código para que la propiedad y los préstamos sean claros.
  • A veces produce mensajes que parecen rígidos, pero suelen guiar a diseños más sanos.


Ejemplo concurrente: Arc<T> + Mutex<T> para compartir datos entre hilos, respetando ownership y seguridad en tiempo de compilación.


En APIs públicas se acostumbra a recibir &T o &mut T para no traspasar propiedad innecesariamente:


fn procesar_entrada(input: &str) {

    // no move; quien llamó conserva ownership

}


Si la función necesita poseer el valor (p. ej. para almacenarlo más tarde), puede aceptar el tipo por valor y dejar claro que la función toma ownership.


Si lo comparamos con los demás lenguajes: 

  • Con lenguajes con GC (Java, C#): el programador no se preocupa por liberar memoria; el runtime limpia por detrás. Ventaja: simplicidad. Desventaja: coste de runtime y pausas, menos control sobre rendimiento. Rust ofrece control y cero-cost abstractions, a cambio de más esfuerzo en la fase de diseño.
  • Con C++ (RAII y move semantics): C++ tiene destrucción automática (RAII) y std::move. Rust toma ideas de RAII y las hace seguras por defecto con el borrow checker. Rust evita UB típico de C++ (use-after-free, double-free) con verificaciones en compilación más estrictas.
  • Con lenguajes dinámicos (Python, JS): la ergonomía es máxima en dinámicos, pero sin garantías en tiempo de compilación. Rust sacrifica algo de ergonomía inicial a favor de seguridad y rendimiento claros.

Errores frecuentes y cómo resolverlos:


1. Intentar usar una variable después de un move

    Síntoma: mensaje value moved here, used here after move.

    Soluciones: pasar por referencia &T, clonar (clone()) si necesitás duplicar, o reorganizar para no necesitarla después del move.


2. Confundir &T y &mut T

   Síntoma: error al pedir &mut mientras hay &.

   Solución: reducir el scope de las referencias previas o copiar/clone si es necesario.


3. Lifetimes complicados

   Síntoma: errores diciendo que lifetimes no coinciden.

   Solución: reestructurar para que la propiedad viva lo suficiente (por ejemplo, mover datos al struct que necesita conservarlos) o usar String en vez de &str para asegurar ownership.


4. Intentar retornar referencia a variable local

   Síntoma: borrow checker lo rechaza.

   Solución: devolver el valor (mover) o hacer que el llamador pase el buffer/propiedad.


Veamos un ejemplo: 


use std::sync::{Arc, Mutex};

use std::thread;


fn main() {

    let contador = Arc::new(Mutex::new(0)); // compartido entre hilos


    let mut handles = vec![];

    for _ in 0..5 {

        let contador_clonado = Arc::clone(&contador);

        let h = thread::spawn(move || {

            let mut num = contador_clonado.lock().unwrap();

            *num += 1;

        });

        handles.push(h);

    }


    for h in handles {

        h.join().unwrap();

    }


    println!("Contador = {}", *contador.lock().unwrap());

}


Aquí combinamos Arc (ownership compartida thread-safe) y Mutex (control de acceso mutable). Rust garantiza que las operaciones sean seguras en tiempo de compilación y ejecución (los deadlocks siguen siendo responsabilidad del diseño).


Ownership y borrowing son el corazón de la promesa de Rust: código seguro y rápido sin un garbage collector.

La curva de aprendizaje existe y vale la pena: te obliga a pensar en cómo fluyen los datos, evitando muchas clases de bugs que en otros lenguajes aparecen en producción.

Si estás diseñando APIs públicas en Rust, piensa en quién debe poseer los datos y cuándo conviene pasar ownership, versus prestar por referencia. Esa decisión marca la ergonomía de la API y la facilidad para usarla de forma correcta.




miércoles, 25 de febrero de 2026

Bases de Datos Relacionales en la Nube

 ¿Qué es una Base de Datos en la Nube?

Una base de datos en la nube es un servicio gestionado que permite crear, administrar y escalar bases de datos sin necesidad de instalar ni mantener infraestructura física.
Estas se ofrecen bajo el modelo DBaaS (Database as a Service), donde el proveedor se encarga de:

  • Infraestructura (servidores, almacenamiento, red).

  • Backups automáticos.

  • Actualizaciones y parches de seguridad.

  • Alta disponibilidad y recuperación ante fallos.

📘 Ejemplos de servicios DBaaS más usados:

  • AWS RDS (Relational Database Service)

  • Azure SQL Database

  • Google Cloud SQL



🔸 Ejemplo: AWS RDS

Amazon RDS (Relational Database Service) permite desplegar motores como:

  • MySQL

  • PostgreSQL

  • MariaDB

  • SQL Server

  • Oracle

  • Aurora (motor optimizado de AWS)

📘 Ventajas específicas:

  • Backups automáticos diarios.

  • Replicación Multi-AZ (alta disponibilidad).

  • Monitoreo con Amazon CloudWatch.

  • Escalado vertical y horizontal sin reinicios.

Ejemplo de creación (consola AWS):

  1. Seleccionar motor (p. ej. PostgreSQL).

  2. Configurar tamaño de instancia (db.t3.micro, db.m6g.large, etc.).

  3. Elegir almacenamiento y backups automáticos.

  4. Configurar seguridad (VPC, grupos de seguridad).

  5. Conectar vía endpoint DNS generado por RDS.



🔸 Ejemplo: Azure SQL Database

Servicio totalmente administrado basado en Microsoft SQL Server.

📘 Características destacadas:

  • Escalado automático según demanda.

  • Alta disponibilidad integrada (réplicas en múltiples regiones).

  • Soporte para autenticación Azure AD y cifrado transparente (TDE).

  • Integración con Power BI y Azure Data Factory.



🔸 Ejemplo: Cloud SQL (Google Cloud)

Permite ejecutar MySQL, PostgreSQL y SQL Server en Google Cloud Platform.

📘 Ventajas:

  • Backups automáticos y restauración en un clic.

  • Replicación regional.

  • Integración con Google Kubernetes Engine y Cloud Run.

  • Seguridad mediante IAM y VPC Service Controls.



Conceptos de Escalabilidad, Alta Disponibilidad y Failover

🔸 Escalabilidad

Capacidad del sistema para aumentar recursos cuando crece la carga.

Tipo

Descripción

Ejemplo

Escalabilidad vertical (scale-up)

Aumentar recursos de una sola instancia (más CPU, RAM, disco).

Cambiar de db.t3.small a db.t3.large.

Escalabilidad horizontal (scale-out)

Agregar más instancias que comparten la carga.

Replicación de lectura en RDS o read replicas en Cloud SQL.

💡 En entornos modernos, se combinan ambas estrategias para mantener rendimiento y disponibilidad.


🔸 Alta Disponibilidad (High Availability, HA)

Garantiza que la base de datos permanezca accesible incluso ante fallos (hardware, red, mantenimiento).

📘 Mecanismos comunes:

  • Replicación síncrona: se mantiene una copia exacta en otra zona o región.

  • Failover automático: si la instancia principal falla, el sistema conmuta a la réplica.

  • Heartbeat y monitoreo continuo: detectan fallas en milisegundos.

📘 Ejemplo (AWS RDS Multi-AZ):

  • Se crean dos instancias en zonas distintas.

  • Si la principal cae, el sistema activa automáticamente la réplica secundaria.

  • El endpoint de conexión se mantiene igual (transparente para la aplicación).


🔸 Failover

Proceso mediante el cual una instancia secundaria toma el control de manera automática cuando la principal falla.

Tipos de failover:

  • Automático: el servicio gestiona la conmutación (RDS, Azure SQL).

  • Manual: el administrador decide cuándo activar la réplica.

📘 Ejemplo visual (RDS Multi-AZ):

[Primary DB - us-east-1a]  replicación síncrona   [Standby DB - us-east-1b]

              (falla)

       [Failover automático a Standby]


🔹 Seguridad en Bases de Datos en la Nube

La seguridad en entornos cloud combina controles de red, cifrado y gestión de credenciales.
La responsabilidad se comparte entre el proveedor cloud y el usuario (Shared Responsibility Model).


🔸 Grupos de Seguridad (Security Groups / Firewalls)

Actúan como cortafuegos virtuales que controlan qué direcciones IP o redes pueden acceder a la base.

📘 Ejemplo:

  • En AWS, un Security Group puede permitir solo el tráfico:

    • Entrante: puerto 5432 (PostgreSQL)

    • Origen: IP de la aplicación o red privada (VPC)

Buenas prácticas:

  • Nunca exponer la base de datos directamente a Internet.

  • Permitir solo el tráfico interno (por VPC o túnel VPN).

  • Revisar reglas de seguridad periódicamente.


🔸 Cifrado en Reposo (Encryption at Rest)

Protege los datos almacenados en disco.
Los principales proveedores cifran automáticamente usando AES-256 o equivalentes.

📘 Ejemplo:

  • AWS RDS usa KMS (Key Management Service) para manejar claves.

  • Azure SQL Database usa Transparent Data Encryption (TDE).

  • Google Cloud SQL usa Cloud KMS.

✅ Los datos, backups, snapshots y logs quedan cifrados automáticamente.


🔸 Cifrado en Tránsito (Encryption in Transit)

Asegura que los datos viajen cifrados entre la aplicación y la base de datos mediante SSL/TLS.

📘 Ejemplo:

psql "host=mydb.rds.amazonaws.com port=5432 sslmode=require user=admin dbname=appdb"

Buenas prácticas:

  • Exigir conexiones con SSL (require_ssl=on).

  • Usar certificados actualizados.

  • Bloquear conexiones no cifradas en la configuración del SGBD.


🔸 Gestión de Credenciales y Acceso

  • Implementar autenticación IAM / AD cuando sea posible.

  • Evitar guardar contraseñas en código fuente.

  • Usar rotación automática de credenciales (por ejemplo, AWS Secrets Manager).

  • Asignar roles mínimos necesarios (principio de least privilege).