Go vs Rust: productividad, rendimiento y complejidad real

Comparación práctica entre Go y Rust para backend y sistemas. Rendimiento, curva de aprendizaje, seguridad de memoria y velocidad de entrega.

Cover for Go vs Rust: productividad, rendimiento y complejidad real

Cada vez que alguien pregunta Go vs Rust en un foro, las dos comunidades se ponen a la defensiva. Los de Go dicen que Rust es innecesariamente complejo. Los de Rust dicen que Go es un lenguaje de juguete. Y así llevan años. Y siendo honestos, ambos tienen algo de razón y algo de exageración.

Vengo del mundo JVM (Kotlin, Java, Spring) y de Python, y llevo un tiempo trabajando con Go y estudiando Rust por curiosidad genuina. Lo que puedo aportar es una perspectiva de alguien que no tiene la camiseta puesta de ninguno de los dos: los he evaluado como herramientas, no como identidades.

Lo que vas a leer aquí es una comparación honesta, con ejemplos reales, sin fanatismo. Vamos a hablar de rendimiento, seguridad de memoria, curva de aprendizaje, tooling, concurrencia y, sobre todo, de cuándo tiene sentido elegir cada uno.


Objetivos diferentes: simplicidad vs control

Lo primero que hay que entender --- y creo que es donde empieza gran parte de la confusión --- es que Go y Rust no compiten por el mismo nicho, aunque se solapen en algunos casos de uso.

Go fue creado en Google para resolver un problema concreto: que equipos grandes pudieran escribir software de servidor rápido, con un lenguaje sencillo de aprender y con tiempos de compilación cortos. Rob Pike, Ken Thompson y Robert Griesemer diseñaron Go para ser aburrido a propósito. Pocas abstracciones, pocas formas de hacer lo mismo, productividad de equipo por encima de la expresividad individual.

Rust nació en Mozilla con otro objetivo: escribir código de sistemas que fuese seguro a nivel de memoria sin necesitar un garbage collector. El foco es el control total: saber exactamente cuándo se asigna y libera memoria, cuándo se hace una copia y cuándo no. Rust quiere que el compilador te impida cometer errores que en C o C++ descubrirías en producción a las 3 de la mañana.

Elegir entre Go y Rust no es elegir el “mejor” lenguaje. Es elegir qué problema estás resolviendo.

Si tu problema es construir un servicio HTTP que responda a peticiones, se despliegue en Kubernetes y lo mantenga un equipo de cinco personas, Go es probablemente la opción más rentable. Si tu problema es escribir un motor de base de datos, un compilador o un sistema embebido donde cada microsegundo importa, Rust tiene ventajas reales. La pregunta interesante no es “cuál es mejor”, sino “qué estoy construyendo”.


Gestión de memoria: GC vs ownership

Esta es la diferencia técnica más profunda entre los dos lenguajes, y la que más consecuencias tiene en el día a día.

Go: garbage collector y a otra cosa

Go usa un garbage collector concurrente con pausas muy bajas (normalmente por debajo de 1 ms). En la práctica, esto significa que no piensas en la memoria. Creas structs, pasas punteros, y el GC se encarga de limpiar lo que ya no se usa.

func createUser(name string) *User {
    u := &User{Name: name, CreatedAt: time.Now()}
    return u // el GC gestiona el ciclo de vida
}

Es simple. Funciona. Y para el 95% de los servicios backend es más que suficiente. Creo que este porcentaje es importante, porque mucha gente optimiza para ese 5% restante sin estar en él.

La contrapartida: el GC introduce un overhead. No es dramático en Go (el equipo de Google lleva años optimizándolo), pero existe. En escenarios de ultra-baja latencia o donde necesitas rendimiento predecible al microsegundo, ese overhead importa. La pregunta que conviene hacerse es: ¿mi caso de uso es realmente uno de esos?

Rust: ownership y borrow checker

Rust no tiene garbage collector. En su lugar, tiene un sistema de ownership que el compilador verifica en tiempo de compilación. Cada valor tiene un dueño, y cuando ese dueño sale de scope, el valor se libera.

fn create_user(name: String) -> User {
    User {
        name,
        created_at: Utc::now(),
    }
    // no hay GC, la memoria se gestiona por ownership
}

El concepto es elegante. Y cuando lo entiendes, tiene una lógica aplastante. Pero la implementación requiere entender borrowing, lifetimes, y cuándo usar &, &mut, Box, Rc, Arc, Clone… y ahí es donde la cosa se complica. Y no un poco.

// Esto no compila:
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// Necesitas anotar lifetimes:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Para alguien que viene de Java, Python o incluso Go, este código es un muro. No porque sea malo, sino porque exige un modelo mental completamente diferente.

Rust te obliga a pensar en la memoria desde el minuto uno. Go te deja olvidarte de ella casi siempre. Ambas son decisiones de diseño válidas.


Curva de aprendizaje: un fin de semana vs varios meses

No hay forma suave de decir esto, así que voy a ser directo: la curva de aprendizaje de Rust es brutal. He hablado con desarrolladores senior con décadas de experiencia que han pasado semanas peleando con el borrow checker. Eso no significa que Rust sea malo --- significa que exige un nivel de inversión que hay que tener en cuenta.

Go: productivo en días

Si ya sabes programar en cualquier lenguaje con llaves, Go se aprende rápido. La especificación del lenguaje cabe en unas pocas páginas. No hay herencia, no hay genéricos complejos (los genéricos de Go 1.18+ son intencionalmente limitados), no hay macros, no hay traits con implementaciones por defecto. Si vienes de aprender Go desde cero, puedes estar escribiendo servicios funcionales en un par de días.

package main

import (
    "fmt"
    "net/http"
)

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

Eso es un servidor HTTP funcional. Sin dependencias externas. Sin framework. Sin configuración.

Rust: productivo en meses

En Rust, el equivalente más mínimo con la librería estándar requiere bastante más ceremonia. La mayoría de la gente usa frameworks como Actix o Axum:

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/health", get(|| async { "ok" }));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

No parece mucho más, pero la realidad es que para llegar a escribir esto con confianza necesitas entender: el runtime asíncrono (Tokio), closures, traits (IntoResponse, Handler), macros procedurales (#[tokio::main]), manejo de errores con Result/unwrap/?… y eso es solo para un endpoint trivial.

AspectoGoRust
Tiempo hasta primer servicio funcional1-3 días2-4 semanas
Tiempo hasta dominio razonable2-4 semanas3-6 meses
Complejidad del modelo mentalBaja-mediaAlta
Documentación y recursos para aprenderExcelenteBuena pero densa
Facilidad de onboarding para un equipoAltaBaja-media

Rendimiento: dónde Rust gana de verdad y dónde Go es suficiente

Esta es la parte donde los benchmarks se sacan de contexto constantemente, y donde creo que se genera más confusión innecesaria. Vamos a intentar ser precisos.

Dónde Rust es objetivamente más rápido

  • CPU-bound puro: parsing, serialización, criptografía, procesamiento de datos en bruto. Rust genera código tan rápido como C/C++. Go no puede competir aquí.
  • Latencia predecible: sin GC no hay pausas. Esto importa en trading de alta frecuencia, motores de juegos, o sistemas en tiempo real.
  • Uso de memoria: Rust permite controlar exactamente cuánta memoria usa tu programa. Go puede consumir más por el overhead del runtime y el GC.

Dónde Go es “suficientemente rápido”

  • APIs HTTP: la diferencia entre responder en 0.8 ms (Rust) y 1.2 ms (Go) es irrelevante cuando tu base de datos tarda 15 ms.
  • Microservicios estándar: CRUD, procesamiento de mensajes, workers. El cuello de botella casi nunca es el lenguaje.
  • Tareas de I/O: si tu servicio espera a la red o al disco el 90% del tiempo, optimizar el código CPU-bound es micro-optimización.
// Benchmark típico: JSON serialization
// Go con encoding/json: ~2.5 μs/op
// Go con jsoniter: ~0.8 μs/op
// Rust con serde: ~0.3 μs/op

Rust es 3-8x más rápido en serialización JSON. Y es tentador mirar esos números y concluir que Rust es la opción correcta. Pero si tu endpoint tarda 50 ms en total porque hay una query a PostgreSQL de por medio, esa diferencia de microsegundos es ruido. Y optimizar el ruido es una trampa en la que es fácil caer.

El rendimiento de Rust es superior. La pregunta real es si tu caso de uso necesita ese rendimiento.

Para escenarios donde Go puede manejar carga pesada sin problemas, puedes consultar Go para tareas pesadas.


Servicios backend: ambos pueden, pero Go entrega antes

Pasemos a lo concreto. En el ecosistema de backend y microservicios, Go tiene una ventaja clara: velocidad de entrega. Y eso, en el mundo real donde los sprints tienen fecha, importa más de lo que la comunidad técnica suele admitir.

Go para backend

  • net/http en la librería estándar es production-ready.
  • Frameworks como Gin, Echo o Fiber son maduros y bien documentados.
  • El ecosistema de drivers (PostgreSQL, Redis, Kafka, gRPC) es sólido.
  • Los binarios son estáticos y pequeños: un contenedor Docker puede pesar 10-15 MB.
  • El tooling (go build, go test, go vet) viene incluido y funciona.
func (h *Handler) GetUser(c *gin.Context) {
    id := c.Param("id")
    user, err := h.repo.FindByID(c.Request.Context(), id)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
        return
    }
    c.JSON(http.StatusOK, user)
}

Si quieres profundizar en cómo Go encaja en arquitecturas modernas, mira Go en cloud-native.

Rust para backend

Rust también puede hacer backend, y lo hace bien. Axum (del equipo de Tokio) y Actix-web son frameworks serios. Pero hay fricciones:

  • Compilación lenta: un proyecto mediano puede tardar 2-5 minutos en compilar desde cero. Go compila el mismo proyecto en segundos.
  • Complejidad con tipos: manejar errores de forma ergonómica requiere crear tipos de error custom, implementar traits como From, y decidir entre anyhow, thiserror, o manejo manual.
  • Menos librerías “listas para usar”: el ecosistema crece rápido, pero hay nichos donde Go tiene opciones más maduras.
async fn get_user(
    State(pool): State<PgPool>,
    Path(id): Path<String>,
) -> Result<Json<User>, AppError> {
    let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id)
        .fetch_one(&pool)
        .await
        .map_err(|_| AppError::NotFound("user not found".into()))?;
    Ok(Json(user))
}

El código Rust es correcto y seguro. No lo discuto. Pero requiere más ceremonia, más tipos custom, y más tiempo para escribir. Y aquí viene la reflexión que me parece más honesta: en un equipo que tiene que entregar features cada sprint, esa ceremonia adicional suma. No porque sea innecesaria, sino porque tiene un coste de oportunidad que no siempre se justifica.


Programación de sistemas: el dominio real de Rust

Pero hay un terreno donde la conversación cambia por completo. Aquí es donde Rust brilla sin discusión, y donde sería injusto no reconocerlo. Programación de sistemas significa:

  • Sistemas operativos (Linux tiene componentes en Rust).
  • Navegadores (Servo, partes de Firefox).
  • Bases de datos (TiKV, SurrealDB, Neon).
  • Herramientas de red (Cloudflare usa Rust extensamente).
  • Compiladores y tooling (el propio compilador de Rust, swc, turbopack).
  • Sistemas embebidos y WASM.

En estos dominios, Rust reemplaza a C/C++ con la ventaja de seguridad de memoria en tiempo de compilación. Y esto no es una opinión --- es un hecho observable. Go simplemente no es una opción aquí porque:

  • El GC introduce latencia impredecible.
  • El runtime de Go consume recursos que en un sistema embebido no tienes.
  • No tienes el control de memoria que necesitas para escribir un allocator o un driver.

Si estás construyendo algo que históricamente se habría hecho en C, Rust es la respuesta moderna. Go no pretende ser esa respuesta.


Tooling y ecosistema

Go

  • go build: compila rápido, binario estático. Sin make, sin CMake, sin Cargo.toml. Sin drama.
  • go test: testing integrado con benchmarks y coverage.
  • go vet y golangci-lint: análisis estático sólido.
  • go mod: gestión de dependencias que funciona sin sorpresas.
  • gofmt: formateo canónico. No hay discusiones de estilo en PRs.
  • gopls: LSP que funciona bien con cualquier editor.

Todo viene incluido o se instala con un comando. La experiencia de tooling en Go es consistente y predecible. No te vas a emocionar con ella, pero tampoco te va a arruinar una mañana.

Rust

  • Cargo: posiblemente el mejor gestor de paquetes y build system que existe. En serio. Es excelente.
  • rustfmt y clippy: formateo y linting de primer nivel.
  • crates.io: el registro de paquetes funciona bien.
  • rust-analyzer: LSP potente aunque a veces pesado en proyectos grandes.
HerramientaGoRust
Build systemgo build (integrado)Cargo (excelente)
Gestor de paquetesgo modCargo/crates.io
Formateogofmtrustfmt
Lintinggo vet + golangci-lintclippy
Testinggo test (integrado)cargo test (integrado)
Velocidad de compilaciónMuy rápidaLenta (mejorando)
Cross-compilationTrivial (GOOS/GOARCH)Posible pero más complejo

El tooling de Rust es objetivamente bueno. Cargo es una maravilla --- probablemente el aspecto de Rust que más envidia sana genera. Pero la velocidad de compilación sigue siendo un punto de fricción real en el día a día. Y no es un detalle menor. Cuando tu bucle de desarrollo es “cambiar una línea, compilar, probar”, esperar 30 segundos (o minutos en builds limpios) cambia tu forma de trabajar. Te vuelves más cuidadoso antes de compilar, sí, pero también más lento para iterar.


Concurrencia: goroutines vs async/Tokio

La concurrencia es uno de los puntos fuertes de ambos lenguajes, pero con filosofías muy diferentes.

Go: goroutines y channels

Go nació con la concurrencia como ciudadano de primera clase. Las goroutines son ligeras (unos pocos KB de stack), se crean con go func(), y se comunican con channels.

func processOrders(orders []Order) []Result {
    results := make(chan Result, len(orders))

    for _, order := range orders {
        go func(o Order) {
            result := process(o)
            results <- result
        }(order)
    }

    var processed []Result
    for range orders {
        processed = append(processed, <-results)
    }
    return processed
}

El modelo es intuitivo: lanzas goroutines, comunicas con channels, y el scheduler de Go se encarga del resto. No necesitas pensar en runtimes, executors ni pinning.

Rust: async/await con Tokio

Rust no tiene un runtime asíncrono integrado. Usas Tokio (el estándar de facto), y el código se basa en async/await con futures:

async fn process_orders(orders: Vec<Order>) -> Vec<Result> {
    let handles: Vec<_> = orders
        .into_iter()
        .map(|order| {
            tokio::spawn(async move {
                process(order).await
            })
        })
        .collect();

    let mut results = Vec::new();
    for handle in handles {
        results.push(handle.await.unwrap());
    }
    results
}

Funciona, y Tokio es impresionante en rendimiento. No tengo ninguna duda de eso. Pero el modelo asíncrono de Rust tiene complejidades que Go simplemente evita:

  • Pinning: algunos futures necesitan estar “pinned” en memoria. Esto añade complejidad que no existe en Go.
  • Send + Sync bounds: cuando compartes datos entre tasks asíncronos, el compilador te exige demostrar que es seguro hacerlo. Correcto, pero verboso.
  • Colored functions: async fn y fn son mundos distintos. Llamar a una función síncrona desde código async (y viceversa) requiere adaptar la interfaz.
  • El error “future is not Send”: probablemente el error más frustrante de Rust. Aparece cuando mantienes una referencia que no es Send a través de un .await.
AspectoGoRust
ModeloGoroutines + channelsAsync/await + Tokio
Runtime integradoNo (Tokio es externo)
Facilidad de usoAltaMedia-baja
Rendimiento crudoExcelenteSuperior
Overhead por tarea~4 KB (goroutine)~menor (future)
Complejidad al compartir estadoMedia (mutex, channels)Alta (Send, Sync, Arc)

Las goroutines de Go son más fáciles de usar. El async de Rust es más eficiente. Para la mayoría de servicios backend, la facilidad gana.


Adopción en equipos: productividad colectiva

Este punto se ignora en la mayoría de comparativas, y creo que es un error grave. Porque en mi experiencia, es el factor decisivo en muchas empresas. No el rendimiento. No el sistema de tipos. El equipo.

Incorporar un equipo a Go

Un desarrollador con experiencia en cualquier lenguaje mainstream puede:

  • Leer código Go el primer día.
  • Hacer PRs funcionales en la primera semana.
  • Sentirse cómodo en 2-3 semanas.

Go tiene pocas formas de hacer las cosas. Eso significa menos discusiones de diseño, PRs más uniformes, y menos tiempo en code reviews discutiendo abstracciones.

Incorporar un equipo a Rust

Un desarrollador nuevo en Rust típicamente:

  • Pelea con el borrow checker las primeras 2-4 semanas.
  • Empieza a ser productivo después de 1-2 meses.
  • Se siente realmente cómodo después de 3-6 meses.

Rust requiere entender ownership, lifetimes, traits, macros, y el ecosistema async. Es mucha superficie cognitiva. No es que sea malo --- es que el ROI del aprendizaje tarda más en llegar. Y hay que ser honestos con eso a la hora de planificar.

En un equipo de 8 personas que necesita entregar un backend en 3 meses, la diferencia entre “productivos en una semana” y “productivos en dos meses” puede definir el proyecto. No es teoría --- lo he visto ocurrir.


Cuándo elegir Rust

Elige Rust cuando:

  • Necesitas rendimiento máximo y latencia predecible (trading, motores de juegos, procesamiento de señales).
  • Estás escribiendo software de sistemas: bases de datos, compiladores, runtimes, drivers.
  • Trabajas con WASM y necesitas código que corra eficientemente en el navegador.
  • El uso de memoria es una restricción dura (embedded, IoT, edge computing).
  • Necesitas seguridad de memoria sin GC, por ejemplo en sistemas críticos.
  • Tu equipo ya conoce Rust o tiene el tiempo para invertir en aprenderlo.

Proyectos como Ripgrep, Alacritty, Deno, SurrealDB y Turbopack demuestran lo que Rust puede hacer en manos expertas.


Cuándo elegir Go

Elige Go cuando:

  • Estás construyendo servicios backend, APIs REST o gRPC.
  • Necesitas CLIs y herramientas de línea de comandos que se distribuyan como un binario.
  • Tu entorno es cloud-native: Kubernetes, contenedores, microservicios.
  • La velocidad de entrega es más importante que exprimir cada nanosegundo.
  • Tu equipo es diverso y necesitas un lenguaje que todos puedan aprender rápido.
  • Quieres binarios estáticos pequeños que se desplieguen fácilmente.

Docker, Kubernetes, Terraform, Hugo, Prometheus, Grafana Loki, CockroachDB… la lista de proyectos exitosos en Go es larga y cubre el nicho de herramientas cloud e infraestructura.

Si vienes de otros lenguajes y quieres empezar, en aprender Go tienes una guía completa.


Comparativa final

CriterioGoRust
Rendimiento CPUBuenoExcelente
Rendimiento I/OExcelenteExcelente
Seguridad de memoriaGC (en runtime)Ownership (en compilación)
Curva de aprendizajeBajaAlta
Velocidad de compilaciónMuy rápidaLenta
ConcurrenciaGoroutines (simple)Async/Tokio (potente)
Ecosistema backendMaduroEn crecimiento
Ecosistema sistemasLimitadoFuerte
Productividad de equipoAltaMedia (tras ramp-up)
Tamaño de binarioPequeño (~10 MB)Muy pequeño (~2-5 MB)
Cross-compilationTrivialPosible, más complejo
ComunidadGrande, pragmáticaApasionada, técnica

Conclusión honesta

No voy a decirte cuál es mejor porque, y sé que suena a tópico, depende de lo que estés construyendo. Pero esta vez el tópico es literal.

Si me preguntas qué elegiría para un backend típico que necesita estar en producción en pocas semanas, con un equipo que viene de Java o Python, la respuesta es Go. No porque sea superior, sino porque la relación entre productividad, rendimiento y simplicidad es difícil de batir para ese caso de uso.

Si me preguntas qué elegiría para escribir un motor de base de datos, un parser de alto rendimiento o una herramienta que necesita exprimir cada ciclo de CPU, la respuesta es Rust. No hay nada moderno que se le acerque en ese espacio.

Lo que no haría --- y aquí me permito ser directo --- es elegir Rust para un CRUD API “porque es más rápido” ni elegir Go para un sistema embebido “porque es más fácil”. Cada herramienta tiene su contexto, y elegir bien el contexto es más importante que elegir bien la herramienta. Suena obvio, pero la cantidad de veces que he visto lo contrario me dice que no lo es tanto.

El fanatismo por un lenguaje es perder el tiempo. Y creo que es una de las cosas que más nos frenan como industria. Lo que importa es entregar software que funcione, que se mantenga y que resuelva el problema real. A veces eso es Go. A veces es Rust. Y a veces --- aunque nos cueste admitirlo --- es Python con un par de scripts y un cron job.

OshyTech

Ingeniería backend y de datos orientada a sistemas escalables, automatización e IA.

Navegación

Copyright 2026 OshyTech. Todos los derechos reservados