Inicio / Artículos / Rust o Go para servicios backend: pague solo por las garantías que necesita

Rust o Go para servicios backend: pague solo por las garantías que necesita

Por qué la seguridad y velocidad de Rust rara vez abordan los verdaderos cuellos de botella de los equipos de API, cómo Go se optimiza para la mantenibilidad y cuándo Rust es la opción adecuada por defecto como backend.

2501 palabras

Rust es uno de los lenguajes más impresionantes de la última década, y precisamente por eso merece una atención detallada antes de convertirse en la opción predeterminada para los equipos de backend. La mayoría de los servicios backend no están limitados por las cualidades que Rust potencia; su limitación radica en la rapidez con la que un grupo en constante cambio puede comprenderlos y modificarlos de forma segura. Este artículo compara a Rust y Go desde esa perspectiva, muestra dónde aparecen realmente los costos de cada lenguaje y le ofrece una forma clara de decidir cuándo vale la pena invertir en el poder de Rust.

Un escenario que merece ser reconocido

Imagínese un equipo implementando una función de backend común en Rust. El código resultante es más seguro, más disciplinado y cuenta con garantías más sólidas en tiempo de compilación que el equivalente en Go. Además, tarda notablemente más en desarrollarse, genera largas discusiones sobre tipos y tiempos de vida, y convierte un cambio rutinario en una discusión sobre el diseño. La misma función en Go se habría entregado rápidamente y sería fácil de entender para cualquier miembro del equipo.

Ninguno de estos resultados significa que un lenguaje sea inferior. Significa que el costo de una herramienta debe sopesarse en relación con el problema al que se aplica.

La genialidad no es lo mismo que la idoneidad

Rust cambió la forma en que la industria percibe la seguridad, el rendimiento y la corrección sin depender de un recolector de basura. Ese logro es real, y merece el respeto que recibe.

Pero la mayoría de los equipos de backend no trabajan en motores de almacenamiento, kernels, navegadores, motores de juego, firmware embebido o infraestructuras sensibles a la seguridad, donde cada asignación y límite de memoria es un problema de máxima importancia. La mayoría está desarrollando APIs. Su trabajo diario consiste en transferir datos JSON entre Postgres, Redis, Kafka, S3, pasarelas de pago, servicios de notificación, sistemas internos y APIs de terceros que fallan de maneras creativas en los peores momentos.

Ese trabajo no es glamoroso: reglas de negocio, intentos de reintentar operaciones, registro de datos, paneles de control, despliegues, migraciones, y ingenieros que intentan mantener la producción estable. Para ese entorno, Rust puede ser una excelente solución a una pregunta que el equipo ni siquiera se planteaba.

Hacer la pregunta correcta

La forma habitual de abordar esto es desde una perspectiva tribal. Un grupo dice que Go es demasiado simplista; otro dice que Rust es demasiado complejo. Ambas observaciones son acertadas, pero ambas se alejan del punto clave.

“¿Es Rust mejor que Go?” es una pregunta demasiado vaga como para responderla. Una sierra de cadena supera a un cuchillo de cocina para talar árboles, pero nadie la lleva a la mesa del comedor. La comparación útil es entre lo que cada lenguaje optimiza:

  • Rust ofrece potencia y precisión, e intenta eliminar categorías completas de errores antes incluso de que el programa se ejecute.
  • Go ofrece moderación y comprensión rápida, y está diseñado bajo la premisa de que ingenieros comunes bajo presión de plazos normales mantendrán el código.

Esa segunda premisa tiene más peso del que parece a primera vista.

¿Qué cambia cuando entra en juego el mantenimiento?

Muchos desarrolladores de Go no desprecian a Rust. Algunos lo admiran, otros lo escriben, algunos quieren aprenderlo, y muchos coinciden sin dudar en que es la opción más adecuada para trabajos serios a nivel bajo. El tono cambia cuando el tema pasa del diseño del lenguaje al mantenimiento a largo plazo del backend.

En ese punto nadie discute que Rust es potente. La pregunta se convierte en si un equipo típico de backend debería asumir los costos de Rust para resolver problemas que sus servicios en realidad no presentan.

Aquí es donde Go se convierte en un competidor fuerte. No porque sea más avanzado, lo cual no es cierto. Ni porque su sistema de tipos sea más rico, tampoco es así. Y desde luego no porque haga que los desarrolladores se sientan inteligentes; tiende a hacer lo contrario. Go gana porque refleja una verdad incómoda sobre las organizaciones de software: la mayoría de los equipos no necesitan código más expresivo. Necesitan código que más personas puedan modificar sin miedo.

Rara vez el cuello de botella es la CPU

A los ingenieros de backend les gusta creer que su sistema está a una sola optimización de la excelencia. Es una historia halagadora, pero por lo general incorrecta.

La mayoría de los backends lentos no lo son debido al entorno de ejecución del lenguaje. Son lentos porque una consulta es ineficiente, un caché proporciona datos obsoletos, la red no es fiable, hay acumulación en colas, una dependencia es inestable, o un flujo de negocio combina cinco operaciones que deberían ser independientes. Un lenguaje más rápido no soluciona ninguno de estos problemas.

La parte difícil de la ingeniería de backends suele no ser hacer que la máquina ejecute instrucciones, sino ayudar a un grupo de personas a comprender el sistema lo suficientemente bien como para poder modificarlo sin dañarlo. El diagrama a continuación ilustra este punto al mostrar dónde suele residir la verdadera fricción:

A Normal Backend Team's Real Bottleneck

          CPU
           |
           |   usually not here
           v

    -----------------
    |  application  |
    -----------------
       |     |     |
       v     v     v

  Postgres  Redis  Kafka
       |       |      |
       v       v      v

  unclear ownership
  changing product rules
  missing observability
  slow code reviews
  fear of refactoring
  tired on-call engineers

The machine was rarely the hard part.
The humans were.

Eso explica cómo Rust puede destacar por sus méritos técnicos y, aun así, ser una mala opción inicial para muchos equipos de backend. Rust se enfoca en la corrección donde el código interactúa con otro código. Go, por su parte, optimiza frecuentemente para sobrevivir en los límites del equipo. Ese contraste resume todo el debate en una sola oración.

La sencillez de Go es una restricción intencionada

Un manejador HTTP típico en Go carece por completo de elegancia. Decodifica el cuerpo de la solicitud, devuelve un 400 si la decodificación falla, llama a un servicio, devuelve un 500 si eso también falla, y de lo contrario escribe el resultado en formato JSON:

func CreateOrder(w http.ResponseWriter, r *http.Request) {
    var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, "invalid request", http.StatusBadRequest)
        return
    }
    order, err := service.CreateOrder(r.Context(), req)
    if err != nil {
        writeError(w, err.Error(), http.StatusInternalServerError)
        return
    }
    writeJSON(w, order)
}

Nadie llamaría a esto el futuro de la programación, pero casi todo desarrollador de backend puede leerlo de inmediato. Un ingeniero junior puede seguirlo línea por línea. Un revisor senior puede aprobarlo en un minuto. Un nuevo empleado puede comprender el flujo de control sin necesidad de explicaciones adicionales. Quien esté depurando un problema puede ver exactamente dónde entra una solicitud, dónde puede fallar y dónde sale.

Esa legibilidad no es una ventaja menor; en muchas organizaciones es lo más importante. Este fragmento también muestra cuán evidentes son los problemas de Go: devolver directamente err.Error() al cliente puede revelar detalles internos como mensajes de la base de datos, y en un servicio real se debería registrar el error y enviar un mensaje genérico en su lugar. La falla es fácil de detectar precisamente porque nada está oculto.

El valor de limitar las decisiones ingeniosas

La experiencia con sistemas de larga duración tiende a disminuir la admiración por la ingeniosidad. Lo que realmente gana respeto es un código simple con puntos de fallo evidentes, un código que no requiere que su autor original lo explique.

Go resulta poco atractivo de una manera útil: restringe la cantidad de decisiones complejas que un equipo puede tomar. Eso suena a crítica hasta que uno ha mantenido una base de código llena de decisiones complejas. Cada abstracción parecía sensata cuando se introdujo. Cada herramienta genérica tenía una buena razón para existir. Cada elección de framework contaba con un argumento convincente. Luego la gente pasó a otras cosas, los requisitos cambiaron y la base de código se convirtió en una colección de decisiones tomadas con confianza en el pasado que nadie comprende del todo.

La intencionada sencillez de Go se opone a esa tendencia. No siempre tiene éxito. El código en Go deficiente es común: manejo duplicado de errores, modelos de dominio simplistas, estado global, carreras de datos, uso de interfaces donde no son necesarias, y context.Context en todos lados sin una comprensión real del concepto de cancelación. La diferencia es que el desorden en Go suele estar a la vista, mientras que el desorden en Rust puede ser mucho más sofisticado. Eso no es un defecto de Rust; es lo que ocurre cuando un lenguaje brinda a ingenieros capaces más espacio para expresar sus habilidades.

Cuando el trabajo simple adquiere una forma compleja

Aquí es donde Rust puede resultar problemático para los equipos de backend. La tarea en sí puede ser trivial, pero los tipos relacionados con ella no necesariamente lo son. La función a continuación procesa un lote de elementos de forma secuencial, esperando un manejador asíncrono para cada uno y deteniéndose al primer error:

use std::future::Future;
trait Processable {
    type Output: Send + 'static;
}
async fn process_batch<F, Fut, T, E>(
    items: Vec<T>,
    handler: F,
) -> Result<Vec<T::Output>, E>
where
    F: Fn(T) -> Fut + Send + Sync + Clone + 'static,
    Fut: Future<Output = Result<T::Output, E>> + Send + 'static,
    T: Processable + Send + 'static,
    E: Send + 'static,
{
    let mut output = Vec::new();
    for item in items {
        output.push(handler(item).await?);
    }
    Ok(output)
}

La lógica es un bucle simple. Sin embargo, la firma debe especificar que el manejador es llamable, clonable y seguro para ser enviado y compartido entre hilos; que el futuro que devuelve es de tipo Send y 'static; y que los tipos de elemento y error cumplen con los mismos límites. Esto no representa un Rust defectuoso ni artificial. Cuando el código asíncrono, los manejadores genéricos, los límites compartidos, las tareas generadas, los tipos de error y los tiempos de vida se combinan, Rust exige que se indique explícitamente lo que otros lenguajes de backend dejan implícito.

Esa explicitidad tiene un valor real, y a veces es precisamente lo que necesita un sistema. Sin embargo, no es gratuita. El costo se manifiesta en el proceso de integración, en las revisiones de código y cada vez que una funcionalidad que parece sencilla desde la perspectiva del producto resulta ser compleja desde el punto de vista del sistema de tipos. También aparece cuando un ingeniero invierte más esfuerzo en convencer al compilador de aceptar una solución que en cuestionar si esa solución debería existir en absoluto.

Mejor, pero mejor en qué?

Los defensores de Rust sostienen que esta fricción genera sistemas mejores, y a veces tienen razón. Un equipo de backend debería plantearse una pregunta más precisa: ¿mejor en qué dimensión?

  • Seguridad de memoria: posiblemente, aunque Go también es seguro en cuanto a la memoria, salvo por las carreras de datos y el uso explícito de unsafe.
  • Rendimiento: con frecuencia.
  • Prevención de ciertos errores de concurrencia: sí, en muchas situaciones.
  • Funcionalidades comerciales habituales de envíos en un equipo con experiencias mixtas durante tres años: no necesariamente.
  • La última dimensión es la que se utiliza para evaluar a los equipos de backend, y es en ella donde la ventaja de Rust no es tan automática.

    El verdadero costo es el mantenimiento, no la autoría

    Un excelente ingeniero en Rust puede crear sistemas excelentes en ese lenguaje. Eso no está en duda. El problema es que las organizaciones no pueden congelar a su equipo en el momento en que cuenta con ese ingeniero. La gente se une y se va, los plazos cambian, los productos evolucionan, y quien diseñó la arquitectura original es promovido, se agota o pasa a otro grupo. El código permanece.

    En ese punto, la elección del lenguaje tiene menos que ver con la elegancia y más con la durabilidad social. Algunas preguntas lo resumen:

    • ¿Puede el próximo ingeniero entender este código?
    • ¿Puede el equipo refactorizarlo de forma gradual?
    • ¿Puede un ingeniero de guardia cansado cambiarlo de forma segura sin tener que memorizar todo el grafo de tipos?
    • ¿Con qué rapidez un nuevo empleado se vuelve productivo?
    • ¿El sistema funciona bien en promedio durante días, y no solo en condiciones ideales?

    Go tiende a dar respuestas más favorables a estas preguntas. No porque los desarrolladores de Go sean más hábiles, ni porque el código en Go sea intrínsecamente limpio, sino porque el lenguaje mantiene su superficie de interacción reducida y deja menos espacios donde la complejidad pueda ocultarse. Esa también es la razón por la que algunos ingenieros lo encuentran frustrante. Go no halaga a sus usuarios. Rust puede hacer que sientas que estás construyendo algo importante, mientras que Go puede hacerte sentir como si solo estuvieras haciendo tareas menores.

    La ingeniería de backend consiste en su mayor parte en tareas similares a las de plomería. El agua necesita fluir, las tuberías deben ser fáciles de localizar, y la próxima persona que trabaje allí debe poder cambiar una válvula sin necesidad de conocer primero toda la historia del edificio.

    Dónde Rust es claramente la herramienta adecuada

    Nada de esto significa que Rust sea necesario en todos los casos. Cuando el rendimiento, la seguridad de la memoria y el control a nivel bajo son fundamentales en lo que se está desarrollando, Rust merece una consideración seria. Si las caídas son inaceptables, si los errores de seguridad de la memoria representan un riesgo para la seguridad, o si la latencia es el producto final y no solo una métrica, Rust podría ser la opción más sensata disponible.

    Los proxies, las bases de datos, los entornos de ejecución de lenguajes, las herramientas de seguridad, los sistemas embebidos, las redes de alto rendimiento, las herramientas para desarrolladores y algunos servicios de infraestructura pertenecen a esta categoría. En esos casos, elegir Rust es una decisión de ingeniería madura.

    Una API de backend estándar, sin embargo, no se vuelve más madura al escribirse en Rust. A veces solo resulta más costosa. Los equipos adoptan Rust por razones sólidas, pero también lo hacen porque Go les parece demasiado sencillo, Java demasiado corporativo, Python demasiado flexible, y Rust les parece la opción adecuada para ingenieros serios. Eso no es un juicio de ingeniería; es inseguridad estética disfrazada de decisión de programación de sistemas.

    Una forma rápida de decidir

    Rust es la opción por defecto adecuada cuando se cumplen la mayoría de estos criterios:

    • el servicio forma parte de la infraestructura donde el rendimiento o el control de memoria son aspectos clave del producto,
    • el equipo ya cuenta con varios ingenieros experimentados en Rust y puede contratar más,
    • un fallo o un error de seguridad en el manejo de la memoria conlleva costos graves desde el punto de vista de la seguridad o los negocios.

    Go, u otro lenguaje que priorice la legibilidad, es la opción por defecto más segura cuando se cumplen la mayoría de estos criterios:

    • el servicio se encarga principalmente de transferir datos entre bases de datos, colas y APIs,
    • el equipo cuenta con experiencias variadas y un alto rotación de personal,
    • los principales riesgos son la falta de responsabilidades claras, los requisitos cambiantes y una baja capacidad de observabilidad, más que un rendimiento bruto insuficiente.

    Conclusión

    Nunca ha habido dudas sobre la excelencia de Rust. Lo importante es determinar si esa excelencia es precisamente lo que carece su equipo de backend.

    Si el equipo está formado por ingenieros expertos en Rust que trabajan en infraestructuras donde las garantías de Rust se corresponden directamente con los riesgos, elija Rust sin dudarlo; utilizar la herramienta más adecuada es lo correcto cuando el trabajo lo exige. Si el equipo desarrolla servicios convencionales que transfieren registros entre almacenes, colas, APIs, paneles de control y herramientas internas, elegir Rust podría indicar más un gusto costoso que madurez técnica.

    Go no es preferible solo porque sea más potente. Para muchos equipos de backend sí lo es, ya que resulta más sencillo de manejar: es más fácil de leer, de contratar a desarrolladores para él, de revisar y desplegar, y también más fácil de mantener sin logros destacados una vez que se desvanece el entusiasmo inicial. No tener logros destacados no significa fracaso; es precisamente lo que necesitan los sistemas en producción mucho tiempo después de que haya pasado el auge inicial.

    Los fallos que realmente arruinan a los equipos de backend rara vez se deben a la falta de un comprobador de préstamos. Proviene de límites de servicio poco claros, intentos de reintentar que no son idempotentes, bases de datos que se han convertido silenciosamente en la verdadera API, colas que han absorbido todos los atajos de diseño, registros que solo cuentan la mitad de la historia, y arquitecturas diseñadas para un equipo idealizado que nunca existió. Rust previene muchos tipos de errores, pero no puede evitar el error de elegir la potencia cuando lo que el equipo necesitaba era claridad. Por eso Rust es una mala opción por defecto para la mayoría de los trabajos de backend: no porque sea débil, sino porque su genialidad tiene un alto costo cuando el problema subyacente se trata principalmente de las personas.

    Lecturas relacionadas