Go: el lenguaje que eligió la simplicidad y conquistó la infraestructura moderna

A finales de la década de 2000, compilar una base de código grande en Google podía tardar minutos… y a veces bastante más. Multiplica esa espera por todo un equipo de ingenieros y de repente tienes un problema caro, frustrante y que se come la productividad. Para colmo, a medida que los proyectos crecían, el software se volvía cada vez más difícil de leer y de mantener.

Go nació precisamente en ese contexto práctico, no en un laboratorio académico. Lo diseñaron Robert Griesemer, Rob Pike y Ken Thompson, y su meta nunca fue crear el lenguaje más expresivo ni el más innovador sobre el papel. Querían algo más modesto y, sobre todo, más útil: resolver problemas reales de productividad y escalabilidad en sistemas de gran tamaño.

En este artículo vamos a repasar las decisiones que definen a Go (incluidas las que dejó fuera a propósito) y por qué terminó convirtiéndose en una pieza clave de la infraestructura actual.

Simplicidad como decisión de diseño

Go es pequeño a propósito. En lugar de ir acumulando características sintácticas cada vez más complejas, se queda con un conjunto reducido de piezas. Así, un programador que ya tiene experiencia en otros lenguajes puede volverse productivo en días, no en semanas.

La lógica es bastante directa: cuantas menos abstracciones innecesarias haya, más fácil resulta leer, entender y mantener el código de otras personas. En equipos grandes y bases de código que viven muchos años, esa legibilidad deja de ser un detalle estético y se convierte en una verdadera ventaja competitiva.

Goroutines: concurrencia casi gratis

El modelo de concurrencia es una de las mayores aportaciones de Go. En vez de depender de los hilos pesados del sistema operativo, el lenguaje ofrece goroutines: unidades de ejecución muy ligeras que se lanzan simplemente anteponiendo la palabra clave go a una llamada de función.

go procesarTarea(datos)

Con esa sola palabra, la función empieza a correr de forma concurrente. El runtime de Go se encarga de repartir las goroutines sobre un número reducido de hilos del sistema, así que es posible mantener miles (e incluso cientos de miles) de tareas simultáneas con un impacto mínimo en memoria y recursos.

Lanzar mil tareas a la vez es tan simple como un bucle. Y con sync.WaitGroup esperamos a que todas terminen:

var wg sync.WaitGroup

for i := 1; i <= 1000; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        procesarTarea(id)
    }(i)
}

wg.Wait() // bloquea hasta que terminen las 1000 goroutines

Channels: comunicar en lugar de compartir

Lanzar miles de goroutines sirve de poco si no tienes una forma limpia de coordinarlas. Para eso existen los channels (canales), inspirados en el modelo de Communicating Sequential Processes. Permiten que las goroutines intercambien datos de forma segura y desacoplada.

resultados := make(chan string)

go func() {
    resultados <- procesarTarea(datos) // envía el resultado por el canal
}()

fmt.Println(<-resultados) // espera y recibe

La filosofía que hay detrás la resumió Rob Pike en una frase que ya se ha convertido en un proverbio del lenguaje:

«Don’t communicate by sharing memory; share memory by communicating.»

Traducido a la práctica: en lugar de sincronizar el acceso a memoria compartida (que suele ser una fuente habitual de bugs difíciles de reproducir), las goroutines se pasan mensajes de forma explícita. El resultado es código concurrente más predecible y mucho más fácil de razonar.

Un patrón muy habitual es el worker pool: varias goroutines toman trabajo de un mismo canal y devuelven sus resultados por otro.

func worker(tareas <-chan int, resultados chan<- int) {
    for t := range tareas {
        resultados <- t * 2
    }
}

func main() {
    tareas := make(chan int, 100)
    resultados := make(chan int, 100)

    for w := 0; w < 3; w++ {
        go worker(tareas, resultados)
    }

    for i := 1; i <= 10; i++ {
        tareas <- i
    }
    close(tareas)

    for i := 1; i <= 10; i++ {
        fmt.Println(<-resultados)
    }
}

Y con select podemos esperar a varios canales a la vez, por ejemplo para poner un límite de tiempo:

select {
case r := <-resultados:
    fmt.Println("listo:", r)
case <-time.After(2 * time.Second):
    fmt.Println("tiempo agotado")
}

Lo que Go decidió no tener

La identidad de Go se define tanto por lo que incluye como por lo que dejó fuera de forma consciente:

  • Sin clases ni herencia. Los datos y el comportamiento se organizan con structs e interfaces, y se prioriza la composición sobre la herencia.
  • Sin excepciones (try/catch). Los errores son valores de retorno explícitos y forman parte del flujo normal del programa. Esto obliga a manejarlos de forma consciente y visible.
  • Sin genéricos en el diseño original. Llegaron años después, con Go 1.18. Omitirlos al principio buscaba preservar la velocidad de compilación y la simplicidad del compilador.

Así se ve el manejo de errores en la práctica:

datos, err := leerArchivo(ruta)
if err != nil {
    return err
}

En lugar de herencia, Go usa interfaces que se cumplen de forma implícita: basta con que un tipo tenga los métodos necesarios, sin tener que declarar que «implementa» nada.

type Notificador interface {
    Enviar(mensaje string) error
}

type Email struct{ Destino string }

func (e Email) Enviar(mensaje string) error {
    fmt.Println("Email a", e.Destino+":", mensaje)
    return nil
}

func avisar(n Notificador) error {
    return n.Enviar("Despliegue completado")
}

Y los genéricos, ya disponibles desde Go 1.18, se ven así:

func Mapear[T, U any](items []T, f func(T) U) []U {
    resultado := make([]U, 0, len(items))
    for _, it := range items {
        resultado = append(resultado, f(it))
    }
    return resultado
}

Nada de esto es una carencia. Son apuestas deliberadas para mantener el lenguaje predecible y el compilador rápido, justo el problema que dio origen a Go.

gofmt y la legibilidad a escala

Go incluye una herramienta oficial de formateo automático llamada gofmt, que elimina casi por completo las discusiones de estilo dentro de los equipos. Como todo el ecosistema pasa por la misma herramienta, el código Go se ve parecido en todas partes, y leer el de otra persona cuesta bastante menos esfuerzo.

Formatear todo un proyecto toma un solo comando:

gofmt -w .

Si sumas las piezas, un lenguaje reducido, errores explícitos, estilo uniforme y pocas abstracciones, el efecto combinado es que un desarrollador nuevo puede leer código ajeno o antiguo con relativa rapidez, incluso en proyectos extensos. Cuando el código tiene que vivir años y pasar por muchas manos, eso vale muchísimo.

Dónde brilla Go

Go se ha consolidado sobre todo en tres terrenos:

  • Servicios backend de alto rendimiento y baja latencia.
  • Herramientas de línea de comandos (CLI), porque genera binarios autocontenidos con mucha facilidad.
  • Infraestructura en la nube, donde su eficiencia de red y su modelo de concurrencia encajan de forma natural.

Los ejemplos más conocidos son Docker y Kubernetes, dos pilares de la infraestructura moderna escritos en Go. Compilar a un único binario estático, sin dependencias externas, simplifica mucho el despliegue y la distribución, y es una de las razones por las que tantos proyectos de este tipo lo eligen.

Un servicio web mínimo cabe en pocas líneas con la librería estándar:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/salud", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "ok")
    })
    http.ListenAndServe(":8080", nil)
}

Y generar un binario estático para Linux, desde cualquier sistema, es una sola línea:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o miservicio

¿Qué tenemos con Go?

Go no compite en expresividad máxima ni en cantidad de características. Su propuesta es pragmática: ser productivo cuando el código tiene que escalar, durar años y ser leído por mucha gente.

Nació de una necesidad concreta en Google y terminó siendo una de las herramientas más relevantes para construir la infraestructura del software actual. En un mundo donde la complejidad crece en silencio, Go sigue recordando que, a veces, menos es más.

Tema:

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.