Característica del sistema de tipos que obliga al programador a manejar explícitamente los valores nulos, evitando los famosos NullPointerException en tiempo de ejecución.
El modelo de concurrencia que me cambió la cabeza
Modelo de concurrencia donde las tareas asíncronas están vinculadas a un scope con ciclo de vida definido. Si el scope se cancela, todas las tareas hijas se cancelan automáticamente.
CoroutineScope, eliges un Dispatcher, marcas funciones como suspend, y lanzas coroutines con launch o async. Es poderoso, pero tiene ceremonia.go antes de cualquier función y ya tienes una goroutine corriendo. No hay scopes, no hay dispatchers, no hay funciones coloreadasConcepto de diseño de lenguajes donde las funciones asíncronas tienen una firma diferente a las síncronas (como suspend en Kotlin o async en JavaScript), creando dos 'colores' de funciones que no se pueden mezclar libremente.
Modelo de scheduling donde M unidades de trabajo livianas (goroutines) se multiplexan sobre N hilos del sistema operativo. El runtime decide automáticamente qué goroutine corre en qué hilo, sin intervención del programador.
import kotlinx.coroutines.*
suspend fun fetchUser(): String {
delay(100) // simulates HTTP call
return "user-data"
}
suspend fun fetchOrders(): String {
delay(150) // simulates HTTP call
return "order-data"
}
fun main() = runBlocking {
val user = async(Dispatchers.IO) { fetchUser() }
val orders = async(Dispatchers.IO) { fetchOrders() }
println("User: ${user.await()}, Orders: ${orders.await()}")
}package main
import (
"fmt"
"time"
)
func fetchUser() string {
time.Sleep(100 * time.Millisecond) // simulates HTTP call
return "user-data"
}
func fetchOrders() string {
time.Sleep(150 * time.Millisecond) // simulates HTTP call
return "order-data"
}
func main() {
userCh := make(chan string)
ordersCh := make(chan string)
go func() { userCh <- fetchUser() }()
go func() { ordersCh <- fetchOrders() }()
user := <-userCh
orders := <-ordersCh
fmt.Printf("User: %s, Orders: %s\n", user, orders)
}runBlocking para entrar al mundo de las coroutines, async para lanzar trabajo concurrente, Dispatchers.IO para elegir dónde corre, y await() para obtener el resultado. En Go, solo necesitas go y un channel. No hay funciones "coloreadas": fetchUser() no necesita ser marcada como suspend, simplemente funciona en una goroutine sin cambiar su firma.Sin funciones coloreadas
suspend ni async. Esto elimina una categoría entera de complejidad: no hay que decidir si una función es "síncrona" o "asíncrona" al momento de diseñarla.Modelo de concurrencia: Kotlin vs Go
Channels: comunicación entre goroutines
Don't communicate by sharing memory; share memory by communicating.
Primitiva de comunicación nativa de Go que permite enviar y recibir valores entre goroutines de forma segura y sincronizada, sin necesidad de locks ni mutexes.
ch := make(chan string) // unbuffered
go func() {
ch <- "hello" // blocks until someone reads
}()
msg := <-ch // blocks until someone sends
fmt.Println(msg) // "hello"ch := make(chan string, 3) // buffer of 3
ch <- "first" // doesn't block
ch <- "second" // doesn't block
ch <- "third" // doesn't block
// ch <- "fourth" would block here until someone reads
fmt.Println(<-ch) // "first"Channel de kotlinx.coroutines (una librería, no parte del lenguaje) o Flow. Y para proteger estado compartido, necesitas MutexMutual Exclusion. Primitiva de sincronización que garantiza que solo un hilo o coroutine pueda acceder a un recurso compartido a la vez. Previene race conditions pero agrega complejidad al código.
select: un statement que permite esperar en múltiples channels simultáneamente y actuar sobre el primero que tenga datos. Funciona como un switch, pero para comunicación concurrente:func fetchWithTimeout(url string) (string, error) {
resultCh := make(chan string)
errCh := make(chan error)
go func() {
resp, err := http.Get(url)
if err != nil {
errCh <- err
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
resultCh <- string(body)
}()
select {
case result := <-resultCh:
return result, nil
case err := <-errCh:
return "", err
case <-time.After(3 * time.Second):
return "", fmt.Errorf("timeout after 3s")
}
}select espera tres cosas a la vez: el resultado, un error, o un timeout. El primero que llegue gana. Este patrón es la base para construir cosas como gateways, donde necesitas llamar a múltiples servicios concurrentemente y manejar timeouts de forma elegante.select es más que un switch
select de Go no tiene equivalente directo en Kotlin. Es la forma idiomática de esperar en múltiples channels, implementar timeouts, cancelaciones y circuit breakers. Si necesitas coordinar múltiples fuentes de datos concurrentes, select es tu herramienta.Patrones: fan-out/fan-in y el gateway
Patrón de concurrencia donde múltiples workers (goroutines) leen del mismo canal de entrada para distribuir y procesar trabajo en paralelo.
Patrón de concurrencia donde múltiples workers escriben sus resultados en un mismo canal de salida, consolidando el trabajo paralelo en un solo flujo.
Servicio que actúa como punto de entrada único para múltiples backends. Recibe requests de los clientes, los enruta a los servicios internos correspondientes, y consolida las respuestas.
package main
import (
"encoding/json"
"net/http"
"time"
)
type GatewayResponse struct {
User UserData `json:"user"`
Orders []Order `json:"orders"`
Inventory []Product `json:"inventory"`
}
func gatewayHandler(w http.ResponseWriter, r *http.Request) {
userCh := make(chan UserData)
ordersCh := make(chan []Order)
inventoryCh := make(chan []Product)
// Fan-out: launch 3 goroutines to different backends
go func() { userCh <- fetchUser(r.URL.Query().Get("userId")) }()
go func() { ordersCh <- fetchOrders(r.URL.Query().Get("userId")) }()
go func() { inventoryCh <- fetchInventory() }()
// Fan-in: collect all results
response := GatewayResponse{
User: <-userCh,
Orders: <-ordersCh,
Inventory: <-inventoryCh,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
func main() {
http.HandleFunc("/api/dashboard", gatewayHandler)
http.ListenAndServe(":8080", nil)
}<-channel. Si cada backend tarda 100ms, el request total tarda unos 100ms, no 300ms. Y como las goroutines cuestan 2KB, puedes tener miles de requests haciendo esto simultáneamente sin que el servicio se inmute.Latencia concurrente, no secuencial
Fan-out/fan-in: API Gateway
async y awaitAll dentro de un coroutineScope. Pero hay una diferencia sutil: en Go, los channels hacen que la comunicación sea explícita y visible en el código. No estás retornando futures y esperando resultados: estás pasando datos por un canal, y eso hace que el flujo de datos del sistema sea más fácil de razonar.Lo que Go resuelve sin que se lo pidas
Archivos ejecutables que incluyen todas sus dependencias compiladas dentro del mismo archivo. No necesitan librerías externas ni un runtime instalado en la máquina destino para funcionar.
go build produce un único archivo ejecutable. Sin JVM, sin dependencias de runtime, sin Docker multi-stage complejo. Lo copias al servidor y funciona. Esto es especialmente poderoso para CLIs y herramientas de infraestructura donde quieres distribuir un solo archivo que funcione en cualquier máquina.Mecanismo que ajusta automáticamente la cantidad de instancias de un servicio según la demanda. Cuando el tráfico sube, se crean nuevas instancias; cuando baja, se eliminan las sobrantes.
go test, go fmt, go vet, go mod vienen con la instalación del lenguaje. No hay debates sobre formateo de código (todos usan gofmt), no hay que configurar linters externos para lo básico, y el sistema de módulos funciona sin sorpresas.Go en números vs Spring Boot
Cuándo sigo eligiendo Kotlin y cuándo elijo Go
Clases restringidas de Kotlin que limitan las subclases posibles a un conjunto conocido en tiempo de compilación. Permiten modelar estados finitos y hacer pattern matching exhaustivo con when.
Domain-Specific Languages. Lenguajes pequeños diseñados para un dominio específico. Kotlin permite crearlos gracias a sus extension functions, lambdas con receiver y convenciones de sintaxis.