Migrar de Go a Rust
(corrode.dev)- La transición de Go a Rust se parece más a elegir trasladar problemas como
nil, el manejo de errores, las carreras de datos y la vida útil de los recursos a garantías en tiempo de compilación que a buscar solo una mejora de velocidad - Go tiene como ventajas la compilación rápida, las
goroutinesimples y un ecosistema fuerte de backend, pero Rust evita más errores desde el sistema de tipos conOption,ResultySend/Sync - El borrow checker de Rust y
async/awaitgeneran una curva de aprendizaje y un costo de usabilidad, y el tiempo de compilación definitivamente debe asumirse como un retroceso frente a Go - Para la transición, conviene una estrategia que empiece por componentes con límites claros, como servicios hot path, workers o algunos endpoints detrás de un gateway, en lugar de una reescritura completa
- Los beneficios esperados se resumen en una reducción de CPU de 20 a 60%, una reducción de memoria de 30 a 50%, una latencia P99 más estable y menos fallas por desreferenciación de
nily condiciones de carrera
El foco de la transición
- Pasar de Go a Rust se parece menos a preguntar “¿Rust es más rápido?” y más a evaluar garantías de corrección, trade-offs de runtime y diferencias en la experiencia de desarrollo
- La comparación se centra en los servicios backend, tomando como referencia las fortalezas de Go: binarios estáticos pequeños, una biblioteca estándar orientada a redes y un ecosistema de servidores HTTP, gRPC y bases de datos
- Parte del contenido también puede aplicar a herramientas CLI, firmware embebido y motores de juego, pero no son el objetivo optimizado
- Como material de contexto relacionado se presentan “Go vs Rust? Choose Go.” de 2017 y “Rust vs Go: A Hands-On Comparison” del equipo de Shuttle
- Go es un lenguaje exitoso, pero decisiones de diseño como el uso extendido de
nil, el manejo de errores apoyado en disciplina más que en tipos y la ausencia prolongada de genéricos se vuelven puntos clave al compararlo con Rust - En la JetBrains Developer Ecosystem Survey, Go aparece como un lenguaje que mantiene una proporción de desarrolladores activos de alrededor de 17–19%, mientras que Rust sigue creciendo pero con una participación menor
Sistema de herramientas
- Tanto Go como Rust tienen un sistema de herramientas con baterías incluidas que ofrece compilación, pruebas, formateo, lint y manejo de dependencias con una interfaz consistente
cargoofrece como herramienta principal un rango más amplio de funciones equivalentes a las herramientas de Gogo.mod/go.sum→Cargo.toml/Cargo.lock: configuración del proyecto y manifiesto de dependenciasgo get/go mod tidy→cargo add/cargo update: agregar y resolver dependenciasgo build→cargo build: compilacióngo run .→cargo run: ejecutar después de compilargo test ./...→cargo test: pruebasgo vet ./...→cargo clippy: linter, yClippyes bastante más opinado quevetgofmt/goimports→cargo fmt: formateador automático sin configuracióngolangci-lint run→cargo clippy -- -D warnings: modo de lint estrictogo doc→cargo doc --open: generación y visualización de documentación APIpprof→cargo flamegraph/samply: perfilado de CPUgovulncheck→cargo audit: revisión de vulnerabilidades basada en bases de datos de avisos
- En Go suele ser común cubrir vacíos con herramientas de terceros como
golangci-lint,mockgen,airygoreleaser, pero en Rust el ecosistema principal cubre más funciones por defecto - Incluso cuando se necesitan crates externos, herramientas como
cargo watchocargo nextestse instalan con una sola vez decargo install cargo-nextesty luego funcionan como herramientas nativas, por ejemplocargo nextest - La mayor ventaja de
gofmtyrustfmt, más que una preferencia por detalles de estilo, es eliminar discusiones de estilo en la revisión de código- Cita de Go Proverbs de Rob Pike: “Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.”
Diferencias clave entre Go y Rust
- Ambos lenguajes son compilados, tienen tipado estático, despliegue en binario único y modelos sólidos de concurrencia, pero la diferencia está en qué garantiza el compilador y en el nivel de control sobre el comportamiento en runtime
- Los principales puntos de comparación son los siguientes
- Lanzamiento estable: Go en 2012, Rust en 2015
- Sistema de tipos: Go tiene tipado estático y estructural, con soporte para genéricos desde 1.18; Rust tiene tipado estático y nominal, con genéricos, traits y lifetimes
- Gestión de memoria: Go usa recolección de basura concurrente y de baja latencia; Rust se basa en ownership y borrowing, sin GC
- Seguridad frente a nulos: en Go
nilexiste ampliamente; en Rust no hay nulos yOption<T>es su reemplazo a nivel de tipos - Manejo de errores: Go usa la interfaz
erroreif err != nil { ... }; Rust usaResult<T, E>, el operador?y pattern matching completo - Concurrencia: Go se basa en
goroutiney canales estilo CSP; Rust usaasync/awaitsobretokio, además de canales e hilos - Cancelación: Go usa
context.Contextpor convención; Rust usa un paso explícito y verificado por tipos, comoCancellationToken - Carreras de datos: Go las detecta de forma probabilística en runtime con
-race; Rust las detecta en tiempo de compilación conSend/Sync - Tiempo de compilación: Go es muy rápido; Rust es lento, especialmente en builds limpios
- Runtime: Go tiene un runtime de alrededor de 2 MB y GC; Rust no tiene runtime más allá de
libco puede compilar completamente estático con MUSL - Tamaño del ecosistema: Go tiene alrededor de 750 mil+ módulos; Rust, 250 mil+ crates
- Validaciones como el manejo de
nil, la propagación de errores, las carreras de datos, la vida útil de los recursos, la cancelación y los genéricos, que en Go dependían de convenciones, herramientas o detección en runtime, en Rust pasan a integrarse en el sistema de tipos - El
Mutex<T>de Rust permite acceder al valor interno solo mediante un guard obtenido con.lock(), eliminando desde los tipos mismos la posibilidad de “olvidar tomar el lock” en alguna ruta - El mismo patrón se repite con
Option,Result,&mut T,Send/Syncy los guards RAII en general, y una vez que uno se acostumbra, el compilador termina reemplazando muchas verificaciones mentales
Limitaciones de Go que llevan a considerar Rust
- Como Go es lo suficientemente rápido para la mayoría de las cargas de trabajo de backend, la razón principal para evaluar Rust se acerca más a la verbosidad del manejo de errores, el riesgo de punteros
nily la falta de funciones sofisticadas del sistema de tipos como enums y traits, que a la velocidad - Las interfaces de Go no son un sustituto suficiente de los traits de Rust, y como la biblioteca estándar no tiene un tipo
Set, se necesitan rodeos idiomáticos comomap[T]struct{} -
Pánicos por
nilen producción- Un servicio en Go puede funcionar normalmente durante meses y luego provocar un pánico de goroutine en una ruta de código específica por no haber verificado un puntero
nil - En el ejemplo,
Finddevuelve(*User, error)y en “not found” elerroresnil, pero la verificación deusersigue siendo responsabilidad del llamador user.Account.Notify()puede colapsar siuseroAccountesnil- Linters y revisiones del IDE como
nilawayystaticcheckdetectan parte de esto, pero son opt-in, probabilísticos y no cruzan de forma confiable los límites entre paquetes Option<T>de Rust evita desreferenciar sin manejar antes el casoNone, eliminando esta categoría de fallas
- Un servicio en Go puede funcionar normalmente durante meses y luego provocar un pánico de goroutine en una ruta de código específica por no haber verificado un puntero
-
Data races que
-raceno detectógo test -racees una herramienta excelente, pero como es un detector en tiempo de ejecución, solo encuentra las carreras que realmente se ejecutaron durante las pruebas- En Go, incluso código donde dos goroutines modifican un map sin locks compila, y puede explotar en producción bajo carga
- En Rust, compartir estado mutable entre hilos requiere tipos que implementen
SendySync, y si intentas compartir unHashMapcomún entre hilos, no compila - Te obliga a usar una de estas opciones:
Arc<Mutex<...>>,Arc<RwLock<...>>o canales, y la condición de carrera se convierte en un error de tipos - Paul Dix menciona explícitamente la eliminación de data races como motivo para reescribir InfluxDB 3.0
- “[The main benefit is] fearless concurrency — eliminating data races essentially, which we had before. Really gnarly bugs in version 1 of Influx due to that.”
- Fuente: Paul Dix, Founder & CTO, InfluxData, Rust in Production
-
Manejo de errores componible
- En Go,
if err != nil { return err }puede diluir la lógica real de la función, y envolver contexto confmt.Errorf("doing X: %w", err)depende de la disciplina más que de reglas del compilador - En el hilo de Lobste.rs, desarrolladores experimentados de Go replican que
errcheckygolangci-lintcapturan la mayoría de los errores omitidos, y que elif err != nilexplícito es más fácil de leer que cadenas densas con? - Peter Bourgon presenta el manejo explícito de errores en Go como un valor cultural intencional
- “I think that error handling should be explicit, this should be a core value of the language.”
- Fuente: Peter Bourgon, GoTime #91, citado en Zen of Go de Dave Cheney
Result<T, E>de Rust forma parte de la propia firma de tipos, así que no se puede olvidar, y con enums definidos mediantethiserror::Errory#[from]se obtiene conversión de errores y verificación de exhaustividad- Si agregas una nueva variant de error, el compilador te indica qué
matchdeben actualizarse
- En Go,
-
Genéricos sin boxing
- Los genéricos de Go 1.18 son útiles, pero tienen limitaciones como la ausencia de métodos con parámetros de tipo, GC shape stenciling y características de rendimiento a veces sorprendentes
- Los genéricos de Rust se monomorfizan, por lo que cada instanciación genera código especializado y no tiene costo en tiempo de ejecución
- Combinados con traits, permiten abstracciones de costo cero
- Esto importa más en infraestructura compartida como middleware, repositorios genéricos, decoders y parsers que en el código de handlers, y en Go a menudo se termina recurriendo a
interface{}/anyy aserciones de tipo en estas áreas
-
Latencia predecible
- El GC de Go es excelente, concurrente, de baja pausa y está bien ajustado para cargas de trabajo típicas de servicios, pero “low-pause” no significa “no-pause”
- En situaciones con muchas asignaciones, la cola de latencia P99 puede ser peor que en una implementación en Rust que no asigna en el hot path
- En sistemas sensibles a la latencia como trading, pujas en tiempo real, proxies de red o ingesta de alto rendimiento, la ausencia de pausas por GC es una ventaja real
- Stephen Blum dice que Rust es necesario para obtener la capacidad de rendimiento por dólar que necesitan a la escala de PubNub
- “Go is great at our scale, but we really need something that is going to give us the price-per-dollar performance capacity that we need, and Rust is going to get us there. That’s why basically everything is heading towards Rust these days.”
- Fuente: Stephen Blum, CTO, PubNub, Rust in Production
Equivalencias en Rust para patrones de Go
- La forma más rápida de familiarizarse con Rust es mapear los patrones de Go que ya conoces a sus patrones equivalentes en Rust
- Hay un ejemplo más largo que implementa el mismo servicio backend en ambos lenguajes en Shuttle comparison
-
Manejo de errores:
if err != nilvsResult<T, E>- En Go, después de
os.ReadFile(path)yjson.Unmarshal, se devuelve un error con contexto usandoif err != nil - En Rust, se compone con
fs::read_to_string(path)?,serde_json::from_str(&data)?yOk(cfg) - El operador
?reemplaza el patrónif err != nil { return err }y, siFrom<E1> for E2está implementado, también maneja la conversión de tipos #[from]dethiserrorda soporte idiomático para esta conversión
- En Go, después de
-
Nulos:
nilvsOption<T>- En Go,
GetUser(id string) *Userdevuelvenilsi no encuentra al usuario, y si quien llama hacefmt.Println(u.Name), ocurre un pánico cuando esnil - En Rust,
get_user(id: &str) -> Option<User>devuelveSome(User)oNone let user = get_user("123"); println!("{}", user.name);da error de compilación porqueuserno esUser, sinoOption<User>- Hay que manejar tanto
Some(u)comoNoneconmatch get_user("123") - En Rust seguro no existe
nil, y las referencias no pueden ser null
- En Go,
-
Interfaces vs traits
- Las interfaces de Go son estructurales, y un tipo satisface una interfaz de forma implícita
- Los traits de Rust son nominales, y deben implementarse explícitamente
- El enfoque de Go es bueno para duck typing improvisado, mientras que el de Rust es bueno para refactorización y discoverability, y permite encontrar con
greplas implementaciones de un trait específico - Una función genérica con trait bound como
fn handle<R: Reader>(r: R)cubre la mayoría de los casos y, con monomorfización, no hay dispatch en tiempo de ejecución - Para almacenar implementaciones heterogéneas cuando sí se necesita dispatch en tiempo de ejecución, se usa
Box<dyn Trait>oArc<dyn Trait>
-
Goroutine vs tarea async
- El modelo de concurrencia de Go es simple, como
go doWork(ctx, input), las goroutines son livianas y el runtime las agenda sobre hilos del SO - Una gran ventaja de Go es que no hay distinción sintáctica entre código secuencial y código paralelo
- En Rust, en servicios backend casi siempre se usa
async/awaitsobre el executortokio - Las funciones async devuelven
Futurey no se ejecutan hasta que se hace await o se lanzan con spawn - El compilador rastrea
Send/Syncantes y después de los puntos.await, y si se retiene un valor non-Sendmás allá de un await, produce un error de compilación - Como no hay desalojo preventivo integrado al estilo de las goroutines, si una tarea CPU-bound se ejecuta demasiado tiempo dentro de una tarea async, puede dejar sin recursos al executor, y hay que pasarla a
tokio::task::spawn_blockingorayon
- El modelo de concurrencia de Go es simple, como
-
context.ContextvsCancellationToken- En Go, se pasa
context.Contexta toda llamada bloqueante - Rust no tiene un
context.Contextintegrado, y el equivalente más cercano para cancelación estokio_util::sync::CancellationToken - El timeout se envuelve sobre el future con
tokio::time::timeout(dur, fut) - El deadline y los valores suelen pasarse mediante argumentos explícitos o spans de
tracing, más que en un único objeto de contexto - Cita de Dave Cheney en The Zen of Go:
- “Go no tiene una forma de decirle a una goroutine que salga. No existe una función stop o kill, por una buena razón. Si no podemos ordenar a una goroutine que se detenga, entonces debemos pedírselo, con cortesía.”
- En Go, esa “petición cortés” es el
context.Contextque se propaga por convención; en Rust, esCancellationTokeno un canalwatch, pero el compilador puede avisar si falta
- En Go, se pasa
-
Cadenas:
stringvsStringy&str- El
stringde Go es un byte slice UTF-8; al asignarlo, se copia el header y se comparten los bytes subyacentes en una estructura inmutable - Rust divide esto en dos tipos
String: es owned, asignado en el heap y growable&str: es una vista prestada sobre datos de cadena de otro lugar, y en la mayoría de los casos corresponde al parámetrostringde Go
- La regla práctica es recibir
&stren los argumentos y devolverStringcuando se crean datos nuevos - La separación entre
&stryStringmuestra en versión reducida el modelo de Rust de “borrow vs own”
- El
Evaluación de los genéricos en Go
- Go introdujo los genéricos en la versión 1.18, en marzo de 2022, 13 años después del lanzamiento del lenguaje
- Se considera que los genéricos son útiles, pero no ofrecen por completo las ventajas que se esperan en Rust, Haskell o C++ moderno, y además arrastran una buena parte de las desventajas de los sistemas de tipos genéricos
-
La biblioteca estándar casi no los usa
- Incluso 3 años después de la introducción de los genéricos, la biblioteca estándar de Go en su mayoría los evita
sort.Slicesigue recibiendo un closurefunc(i, j int) boolen lugar de un constraintcmp.Orderedsync.Mapsigue tipado comoany/any- Los helpers genéricos que existen están en unos pocos paquetes, como
slices,maps,cmpy algunos elementos bajosync - El compromiso de compatibilidad de Go 1 explica en parte por qué es difícil adaptar APIs no genéricas existentes, pero aun así no usa los genéricos como herramienta principal, a diferencia de Rust
- En Rust, los genéricos están presentes desde el inicio en
Option<T>,Result<T, E>,Vec<T>,HashMap<K, V>,Iterator,From/Into, y en todas las colecciones y smart pointers
-
No tiene sistema de traits y solo hay constraints estructurales
- Los genéricos de Rust están ligados a traits que manejan polimorfismo ad hoc, supertraits, associated types, blanket impl y coherence
- Los constraints de Go se parecen más a interfaces con el operador
~agregado para la pertenencia a conjuntos de tipos - En Go no existen jerarquías de supertraits como
trait Ord: Eq + PartialOrdde Rust, associated types comotype Item;deIterator, ni blanket impl comoimpl<T: Display> ToString for T - En Go no se pueden usar métodos con parámetros de tipo, así que no es posible algo como
func (s Set[T]) Map[U](<https://corrode.dev/learn/migration-guides/go-to-rust/f func(T>) U) Set[U] - En cuanto la abstracción va más allá de “una función que opera sobre cualquier
Tcon unas cuantas operaciones”, Go vuelve aany, aserciones de tipo, generación de código o reflection en tiempo de ejecución
-
Diferencias en inferencia de tipos y estrategia de implementación
- Rust propaga información de tipos a lo largo de toda la expresión, incluyendo closures, cadenas de iterators y el operador
? - La inferencia en Go es más superficial: por lo general infiere parámetros de tipo a partir de los argumentos de función, pero no puede inferirlos desde el contexto de retorno, y suele exigir argumentos de tipo explícitos en el sitio de llamada
- Go eligió un camino intermedio llamado GCShape stenciling and dictionaries para mantener tiempos de compilación rápidos, pero eso puede introducir indirección en cada llamada a métodos sobre parámetros de tipo
- Como material que lo muestra, se cita el artículo de PlanetScale
- Rust genera código máquina especializado para
Vec<i32>yVec<String>por separado, sin despacho en tiempo de ejecución - El costo de la monomorfización es el tiempo de compilación, y ambos lenguajes optimizan objetivos distintos
- Rust propaga información de tipos a lo largo de toda la expresión, incluyendo closures, cadenas de iterators y el operador
-
No logra tapar los huecos del sistema de tipos
- En Rust, los genéricos y los traits eliminan la mayoría de las situaciones en las que haría falta
Box<dyn Any>o reflection en tiempo de ejecución - Los genéricos de Go no eliminan
any,reflectni los patrones dominantes de generación de código en ORM, decoders y mocks encoding/jsonsigue usando reflection,database/sqlsigue usandoanyymockgensigue generando código- Los genéricos de Go se sienten como una herramienta nueva útil en casos acotados, mientras que en Rust funcionan como una base cuya ausencia haría que el lenguaje se derrumbara
- En Rust, los genéricos y los traits eliminan la mayoría de las situaciones en las que haría falta
Ecosistema backend de Rust
- El ecosistema de Rust también ha convergido hasta cierto punto en torno a “opciones por defecto” para servicios backend comunes
- Relaciones representativas:
- HTTP server: Go
net/http,chi,gin,echo,fiber→ Rustaxumsobrehyper - HTTP client: Go
net/http,resty→ Rustreqwest - gRPC: Go
google.golang.org/grpc+protoc-gen-go→ Rusttonic+prost - SQL: Go
database/sql,sqlc,sqlx,gorm→ Rustsqlx,sea-orm,diesel - Migrations: Go
golang-migrate,goose→ Rustsqlx migrate,refinery - JSON: Go
encoding/json,sonic,goccy/go-json→ Rustserde+serde_json - Logging: Go
log/slog,zerolog,zap→ Rusttracing+tracing-subscriber - Metrics: Go
prometheus/client_golang→ Rustmetrics+metrics-exporter-prometheus - Config: Go
viper,koanf→ Rustconfig/ config-rs,figment - CLI: Go
cobra,urfave/cli→ Rustclapderive - Errors: Go
errors,pkg/errors→ Rustthiserrorpara librerías,anyhowpara binarios - Testing: Go
testing,testify,gomega→ Rust#[test]integrado,rstest,assert_matches - Mocking: Go
mockgen,moq→ En Rust son idiomáticos los fakes escritos a mano, y también se usamockall - Background tasks: Go goroutines +
errgroup→ Rusttokio::spawn+JoinSet
- HTTP server: Go
- En un servicio backend típico, se plantea que la combinación
axum+sqlx+tokio+tracing+serde+clapcubre el 90% de lo necesario
El verificador de préstamos y la curva de aprendizaje
- Hay que partir de la base de que al pasar de Go a Rust te vas a estrellar contra una pared
- El runtime de Go se encarga de la memoria y el aliasing, pero Rust traslada esas decisiones al sistema de tipos, por lo que durante las primeras semanas el compilador puede rechazar código que “obviamente debería funcionar”
- Patrones con los que los desarrolladores de Go suelen chocar:
- Referencias de larga duración: en Go es natural conservar por mucho tiempo un
*Usersacado de un mapa, pero en Rust no se puede modificar el mapa mientras ese préstamo siga vivo - Structs autorreferenciales: en Go puedes poner en el mismo struct los datos y un iterador sobre esos datos, pero en Rust hace falta
Pin,ouroboroso un rediseño - Estado mutable compartido entre goroutines: el patrón
mu sync.Mutex; data map[K]Vde Go se convierte en Rust en algo comoArc<Mutex<HashMap<K, V>>> - Devolver referencias desde funciones: aparecen las anotaciones de lifetime, un concepto nuevo para quienes vienen de Go
- Referencias de larga duración: en Go es natural conservar por mucho tiempo un
- Hay que ver al verificador de préstamos no como un “portero” que estorba, sino como un mecanismo que revela bugs reales
- Filtra en tiempo de compilación casos donde un valor se vuelve a usar después de haberse movido, varios hilos tocan los mismos datos al mismo tiempo, se desreferencian punteros nulos o colgantes, o una referencia vive más que el valor al que apunta
- Una vez que se internaliza el concepto de préstamo, deja de ser algo contra lo que pelear y pasa a ser un aliado; los desarrolladores experimentados de Rust suelen decir que entre las 4 y 12 semanas el verificador de préstamos ya se volvió una ayuda
- Stephen Blum, CTO de PubNub, dijo en Rustacean Station que el primer mes “se sintió como aprender a programar por primera vez”, y que tuvo que lidiar a la fuerza con el verificador de préstamos y los lifetimes
- Ed Page, maintainer de
clap, dijo en Rustacean Station: clap with Ed Page que el verificador de préstamos le permitió enfocarse en problemas de más alto nivel y además detectó cosas que él mismo no había logrado analizar
Principales dificultades de la transición a Rust
-
Tiempo de compilación
- El tiempo de compilación de Rust debe asumirse como un retroceso claro frente a Go; un build limpio de release para un servicio de tamaño medio puede tardar varios minutos, a diferencia de la compilación casi instantánea de Go
- Los builds incrementales y
cargo checkson razonables, y el tiempo de compilación ha mejorado cada año, pero la diferencia frente a Go se siente - En el ciclo de edición conviene usar
cargo check, separar en workspaces cuando empiece a aportar valor, y mantener en crates aparte los crates con muchas macros procedurales para que solo se recompilen cuando cambien - Para más detalle, se puede consultar consejos para reducir el tiempo de compilación en Rust
-
El problema del coloreado asíncrono
- La separación entre
async fnyfnen Rust es una de las mayores regresiones de usabilidad al venir de Go - async trait se estabilizó desde Rust 1.75, pero todavía tiene asperezas cuando se mezcla con despacho dinámico
- En algunas situaciones se termina usando el crate
async-traitpara cubrir esas carencias
- La separación entre
-
Un ecosistema más pequeño
- El ecosistema de crates de Rust está creciendo y la calidad de las librerías es en general alta, pero Go lleva ventaja en algunas áreas cercanas al backend
- Áreas donde Go va adelante incluyen operadores de Kubernetes, SDK de proveedores cloud y drivers de bases de datos para ciertos sistemas de almacenamiento de nicho
- Antes de decidir la migración, conviene dedicar al menos un día a verificar si las librerías de las que dependes tienen alternativas utilizables en Rust
- Algunos equipos quizá tengan que actualizar un crate abandonado de validación de esquemas XML o escribir por su cuenta clientes para protocolos menos conocidos
Estrategias de integración
- Una transición exitosa de Go a Rust no se parece a reescribirlo todo de una sola vez, sino a una serie de decisiones tácticas
- Victor Ciura, Principal Engineer de Microsoft, dijo en Rust in Production que “no se trata de reescribir todo en Rust por diversión, sino de una decisión táctica: si un componente nuevo encaja mejor en Rust, entonces se hace en Rust”
-
1. Separar el hot path como servicio
- Si un servicio específico sigue causando problemas, la migración de menor riesgo es reescribir solo ese servicio en Rust y dejarlo detrás del mismo contrato de API
- El objetivo puede ser un servicio con alto uso de CPU, sensible a la latencia o con problemas recurrentes de estabilidad
- Los demás servicios en Go siguen comunicándose por HTTP/gRPC, así que no necesitan saber cuál es el lenguaje de implementación interno
- Jeff Kao, CTO de Radar, dijo en Rust in Production que el texto de Discord sobre su paso de Go a Rust le hizo pensar en intentar lo mismo en Radar
-
2. Reemplazar sidecars o procesos worker
- Los workers de fondo, consumidores de colas, pipelines de recolección y trabajos batch CPU-bound son buenos primeros objetivos
- Por lo general tienen límites claros de entrada y salida, como una cola o un topic, y no comparten estado in-process con el resto del sistema
-
3. cgo es posible, pero doloroso
- Desde Go se puede llamar a Rust mediante cgo, y hasta hay una buena guía sobre esto
- En servicios backend normalmente no se recomienda
- La complejidad del build y el overhead del FFI a menudo cancelan cualquier ventaja frente a “levantar un servicio en Rust y ponerlo detrás de una llamada de red”
- En librerías y herramientas CLI puede ser más práctico
-
4. Aplicar el Strangler Pattern detrás de un gateway
- Si tienes un API gateway o un reverse proxy, puedes enrutar solo ciertos endpoints al nuevo servicio en Rust y dejar el resto en Go
- Encaja especialmente bien cuando un solo bounded context, como autenticación, búsqueda o pagos, sirve como unidad de migración
- A este patrón se le llama “strangler fig” porque el nuevo servicio crece alrededor del existente hasta reemplazarlo por completo
Consejos prácticos para la migración
- Hay que empezar por servicios con límites claros, y no elegir el servicio más central ni el que más despliegues tiene
- Conviene elegir un servicio con un contrato bien definido con el resto del sistema y con un radio de impacto pequeño
-
Mantener el mismo contrato de API
- Si el servicio en Go expone una API REST, el servicio en Rust también debe mantener las mismas rutas, la misma forma de JSON y los mismos wrappers de error
- Para los clientes la migración es invisible, y el tráfico puede desviarse gradualmente mediante el gateway
-
No trasladar literalmente los modismos
if err != nil { return err }se convierte en?- El patrón de una goroutine por request solo se traslada a
tokio::spawncuando de verdad hace falta axumya maneja requests en paralelo- Las interfaces de un solo método normalmente pasan a ser trait bounds en genéricos, no
Box<dyn Trait>
-
Usar el compilador como si fuera un compañero de pair programming
- Los errores del compilador de Rust suelen ser de buena calidad y, si se leen con calma, casi siempre señalan la respuesta correcta
- Quienes más tardan en avanzar suelen ser los miembros del equipo que no ven al compilador como un colaborador, sino como un adversario
-
Invertir en capacitación desde el principio
- Las migraciones a Rust que intentan aprenderse “sobre la marcha” muchas veces no terminan bien
- Hay que reservar tiempo real de aprendizaje, ya sea con talleres, cursos en línea o sesiones de pair sobre código real
- Cuando el equipo adquiere soltura, esa inversión inicial se recupera varias veces
Áreas donde Go sigue siendo una buena opción
- No hace falta mover todo a Rust, y hay áreas donde Go sigue siendo especialmente bueno
-
Herramientas nativas de Kubernetes
- El espacio de operadores, controladores y CRD está abrumadoramente centrado en Go dentro del ecosistema
-
Utilidades CLI y herramientas de desarrollo
- Sus fortalezas son la compilación rápida, la compilación cruzada sencilla y el despliegue simple
-
Servicios glue
- En capas API delgadas, proxies y transformadores de formato, la proporción de boilerplate de Rust puede no valer la pena
-
Donde la velocidad del equipo importa más que la garantía absoluta de exactitud
- En áreas donde hay que moverse rápido, Go puede seguir siendo una buena opción
- Jon Seager, VP of Engineering de Canonical, dice en Rust in Production que Go es una muy buena opción para servicios de networking, que en Canonical hay mucho Go y que Juju también es una enorme base de código en Go
- Una estrategia híbrida es común, y muchos equipos terminan con backends multilenguaje: Go para servicios “aburridos” y Rust para servicios donde la estabilidad y el rendimiento compensan el esfuerzo adicional
Mejoras que puedes esperar
- Como las cifras varían mucho según la carga de trabajo, esto debe verse como una guía aproximada y no como una promesa
- Rangos aproximados de mejora observados en migraciones de Go a Rust:
- Uso de CPU: reducción de 20~60%
- Como Go ya es eficiente, no suele ser tan dramático como pasar de Python a Rust
- Las ganancias vienen de la ausencia de GC y de loops más ajustados
- Memoria: reducción de 30~50%
- Principalmente porque no hay sobrecarga de GC y el runtime es más pequeño
- Latencia P99: mucho más consistente
- Los servicios en Rust tienden a aplanarse y a reducir el jitter provocado por el GC que sí se ve en servicios en Go
- Del lado de Go hubo mucha mejora tras la introducción de GC de baja latencia, pero bajo carga alta todavía queda diferencia
- Incidentes en producción: es el área de mejora que los equipos reportan con más fuerza
- Tipos de bugs como data races, desreferencias de nil y rutas de error faltantes, que pueden pasar
go test -racey llegar hasta producción, no compilan en Rust - Después de migrar a Rust, las guardias on-call suelen volverse bastante aburridas
- Tipos de bugs como data races, desreferencias de nil y rutas de error faltantes, que pueden pasar
- Uso de CPU: reducción de 20~60%
- Andrew Lamb, Staff Engineer de InfluxData, comenta en Rustacean Station: Rebuilding InfluxDB with Rust que, después de reescribir InfluxDB, ya no tuvieron que rastrear crashes, extrañas race conditions multihilo ni problemas que antes les consumían mucho tiempo
- Es poco probable que pasar de Go a Rust mejore el throughput 10 veces, como sí puede pasar al migrar de Python a Rust
- El beneficio real está en menos “errores absurdos”, colas de latencia más planas y la capacidad de expandirse a otras áreas, como desarrollo embebido o programación de sistemas, usando el mismo lenguaje
Notas complementarias
- El sistema de tipos de Rust no elimina todos los bugs de lógica de sincronización, pero los tipos que no se pueden compartir entre hilos sin sincronización simplemente no compilan
- El tipo de problema donde “olvidaste poner el lock” termina en corrupción silenciosa de datos es algo que el sistema de tipos de Rust puede evitar
- El
stringde Go es una secuencia inmutable de bytes y por convención es UTF-8, pero eso no está garantizado a nivel de tipo - La correspondencia más cercana es Go
string↔ Rust&strpara vistas de solo lectura, y Go[]byte↔ RustVec<u8>para buffers mutables Stringen Rust es la versión con ownership y expandible de&str, con la garantía adicional de que el contenido es UTF-8 válido- Para más detalles, puedes consultar Strings, bytes, runes and characters in Go
- Desde Go 1.18 son posibles las funciones genéricas y los tipos genéricos, pero no se introdujeron parámetros de tipo en los métodos en sí
- Las cadenas de iteradores de Rust, como
(0..100).filter(|n| ...).collect(), pueden resultar extrañas para desarrolladores de Go, pero en Rust también se pueden usar loopsfor, y en código de una sola vez suelen ser la elección correcta
Conclusión
- Pasar de Go a Rust es distinto de pasar a Rust desde Python o TypeScript
- Los desarrolladores que vienen de Go ya conocen las ventajas del tipado estático y de los lenguajes compilados, así que no se trata de una transición que abandona tipado dinámico o runtimes lentos
- El intercambio central es dejar atrás
nily obtener una base de código más sólida, menos trampas y un compilador más estricto que detecta más errores en tiempo de compilación - A cambio, la curva de aprendizaje es más empinada
- En servicios críticos para el negocio, de los que depende la organización, que requieren alta disponibilidad, como el software fundacional, ese intercambio vale claramente la pena
- En otros servicios, Go puede seguir siendo la respuesta correcta
- El objetivo de la migración es ubicar cada problema en el lenguaje que mejor lo resuelve
1 comentarios
Comentarios en Hacker News
Entiendo migrar de C/C++ o Python a Rust por varias razones, pero para un backend web Go parece una muy buena elección
Uso casi puro Rust, pero la última vez que trabajé en servidores web con Rust sentí que habría sido mejor usar Go
El texto original señala que la sintaxis de manejo de errores de Go es verbosa, y es una crítica válida. Rust tuvo el mismo problema y luego agregó la sintaxis
?para devolver el valor de error cuando hay un error. El manejo de errores en Go es, en gran parte, la forma expandida de esoEn Rust no hay un tipo de error unificado, y existen esquemas de error importantes como
io::Error,thiserroryanyhow, lo que vuelve engorroso propagar errores hacia arriba siguiendo la cadena de llamadasHay cosas que, si se omiten en un lenguaje nuevo, luego son difíciles de agregar. Tipos constantes, tipo booleano, tipo de error, tipos de arreglos multidimensionales, tipos de vectores y matrices de tamaño 2/3/4 y operaciones estándar. Si no se estandarizan al principio, se pierde mucho tiempo conciliando distintas representaciones del mismo concepto
Fuera del manejo de errores, esto afecta menos al desarrollo web, pero en cálculo numérico, gráficos y modelado se vuelve un gran dolor porque hay que aplicar operaciones estándar a arreglos numéricos
Go tiene dos ventajas en servicios web. La primera son las goroutines que menciona el texto, y la segunda, que el original no cubre tanto, son las bibliotecas. Go tiene la mayoría de las bibliotecas necesarias para servicios web, y muchas se usan dentro de Google, así que han sobrevivido entornos muy duros. En cambio, muchos crates de Rust son menos maduros y a menudo no tienen aseguramiento formal de calidad
Además, Rust todavía depende de muchas bibliotecas C/C++ en comparación con Go, así que la compilación cruzada, los builds reproducibles y la generación de binarios estáticos suelen volverse problemáticos
La desventaja de Go es que su recolector de basura es demasiado simple. Si aparecen picos de latencia, fuera de una reescritura dolorosa hay pocas formas de reaccionar
Lo que listaste son solo formas comunes de usarlo, y usar
Boxpor sí solo no causa ningún problema. Eso se parece bastante a lo que haceanyhow::ErrorAun así, en cuanto a la biblioteca estándar, creo que Go lo hizo mucho mejor que Rust
Me gusta el lenguaje Rust y lo uso para firmware embebido y aplicaciones de PC, pero para backend web sigo usando Python. La razón es que Rust no tiene un conjunto de herramientas al nivel de Django o Rails
Hay cosas parecidas a Flask, pero no existe el ecosistema sólido de Flask. Tengo poca experiencia con Go, pero para backend web probablemente elegiría Go antes que Rust. La razón es el ecosistema de bibliotecas y frameworks
Además, por las razones habituales, Async Rust no me gusta mucho. En el ecosistema web de Rust casi todo exige usar asincronía
io::Errores solo uno de muchos tipos que lo implementan, no tiene nada especial. Los errores definidos conthiserrortambién implementan ese traitanyhowsolo permite decir cómodamente “algún Error” cuando no quieres describir en detalle como contrato de API qué tipo de error puede emitir una funciónRust hace más fácil que Go escribir código determinista, así que es muy útil cuando necesitas pruebas de simulación determinista y pruebas basadas en propiedades
Hace poco escribí en Go una herramienta de mirroring de datos de Postgres a Iceberg https://github.com/polynya-dev/pg2iceberg, pero la porté a Rust porque quería hacer pruebas de simulación determinista sin pelear con el runtime de Go
Dicho eso, si ese dominio no es lo bastante importante como para justificar ese nivel de pruebas, elegiría Go antes que Rust en cualquier momento
Artículo relacionado: https://www.polarsignals.com/blog/posts/2024/05/28/mostly-ds...
Puede sonar obvio y repetitivo, pero mi mayor queja con Rust es la situación del manejo de paquetes, y creo que es completamente resultado de la mentalidad de los desarrolladores
Me gusta la usabilidad de Rust. El enfoque funcional sobre tipos de datos es hermoso. Pero ahora mismo estoy trabajando en paralelo con un proyecto en Rust y otro en Go, y el árbol de dependencias es un animal completamente distinto
El proyecto en Go se resuelve en su mayor parte con la biblioteca estándar, pero en el proyecto en Rust pedí solo
rusqlite(sqlite),clap(CLI),ratatui(TUI) ytauri(GUI), y parece que ya pasan de 400 dependencias. En especialtauries por mucho el principal responsable, pero incluso sin eso casi llegan a 100, y se siente una locuraSería mucho mejor si hubiera alternativas de crates de Rust bien mantenidas que manejaran las dependencias de forma razonable, pero todavía no las encuentro. Yo solo no quiero meter un shai hulud en el sistema, pero la gente del mundo web de Rust parece querer convertir
cargoen algo comonpmen ese aspectoPor eso el número de dependencias parece mayor de lo que realmente es. Aunque sean crates separados, muchas veces tienen el mismo mantenedor y son parte del mismo repositorio Git upstream
Aun así, coincido con la sensación general. En Rust hay bastantes crates 0.x medio abandonados, y con frecuencia no hay una alternativa mejor
Y después de eso aparece
httplib3y luegohttplib4Dicho eso, prefiero mucho más el enfoque de Rust. Para mí no hay una gran diferencia entre depender de la biblioteca estándar o de otra dependencia. Al final sigue siendo una dependencia
Pensar que por ser biblioteca estándar su calidad es mayor o su mantenimiento mejor es mezclar conceptos distintos
Al final todo depende de los recursos. Claro, la biblioteca estándar puede recibir más recursos, pero también puede volverse gigantesca e imposible de mantener
rusqlite,clap,ratatuiytauri, fuera quizá de JavaTambién hay que ver que Tauri en sí está compuesto por 14 crates, y cada uno aparece en el árbol de build
https://github.com/tauri-apps/tauri/blob/dev/Cargo.toml
Ratatui también son 6
https://github.com/ratatui/ratatui/blob/main/Cargo.toml
Nadie lo ha “resuelto”, y dudo que llegue a existir una sola solución
En Go hay que confiar en que los desarrolladores de bibliotecas respeten correctamente el versionado semántico, y no puedes fijar versiones. En lo personal eso me molesta bastante
Hay algunos atajos. Puedes usar un SHA como el hash de un commit de Git para crear una especie de versión, o usar vendoring, que es un caché conocido de dependencias. Pero vendoring trae consigo problemas de gestión de caché
Este fin de semana tuve que usar entornos virtuales de Python y no terminó bien; me recordó por qué me alejé de Python
El CPAN de Perl, Maven/Gradle de Java, gems de Ruby, dep/glide/vgo/modules de Go, Cargo de Rust, npm/yarn de Node, todos tienen problemas parecidos
Lo mismo pasa con los sistemas operativos: yum/rpm de Redhat, apt de Debian, snap de Ubuntu. En especial no entiendo qué pasa con snap
Según el caso de uso, quizá tendría sentido dejar el frontend en Go y mover solo el backend a Rust
Este texto se siente raro porque intenta ser a la vez una guía de migración y un texto de defensa de Rust
Al final, si estás pensando si usar Rust o Go, la cuestión central se reduce casi por completo a “¿quieres un runtime administrado?”. Toda una generación de programadores de Rust se ha convencido de que un runtime administrado es malo, y que no tenerlo es una característica importante
Pero eso es claramente falso. Hay más áreas de programación donde sí quieres un runtime administrado que áreas donde no lo quieres
Eso no significa que Go deba ser siempre la opción por defecto en esos casos. También hay muchas razones subjetivas para preferir Rust. Cuando uso Go extraño
match, pero no extrañotokioni Async RustAmbos son opciones válidas en casi cualquier caso donde no haga falta torcer a la fuerza el espacio del problema. Por ejemplo, intentar escribir un módulo del kernel de Linux en Go sí sería una elección extraña
La pelea Rust vs Go parece una frontera rara y algo vergonzosa de nuestro campo. Una gran parte de la industria construye sistemas completos perfectamente bien con Python o Node, y se ríe de los bichos raros que discuten qué lenguaje compilado con tipado estático usar. La pregunta real es Python vs Rust/Go, no Rust vs Go
Pero en general, la gente de Rust y Go debería unir fuerzas contra el mal del tipado dinámico. Si ahora los type hints se consideran una buena práctica, ¿no es eso básicamente admitir que era un defecto?
Incluso con buenos type hints, eso sigue siendo inferior a la inferencia de tipos. La inferencia de tipos te permite dejar intacto mucho código cuando cambias tipos, y al mismo tiempo evita cambios de tipo no intencionales
Ojalá TS tuviera un poco más de runtime. Lo único que envidio de Python es que puede validar esquemas JSON en endpoints HTTP de una manera muy natural
El proceso de tener que pasar por Zod sigue siendo una fuente constante de fastidio, y me parece un problema causado por lo dogmático que es el equipo de TS
Las huellas de la escritura con LLM son cada vez más sutiles, pero todavía se notan muchísimo. En particular la palabra genuine
Cosas como “This is the area where Go genuinely shines, and it’s worth being precise about why”, “the lack of GC pauses is a genuine selling point”, “Humans are genuinely bad at reasoning about memory”, “There are cases where the borrow checker is genuinely too strict”
No creo que todo el texto haya sido generado por IA, pero sí que recibió ayuda de IA. Y si fue así, el autor lo hizo genuinely bien
Como no veo a otros mencionándolo, parece que no daña demasiado el contenido en sí, pero se siente extraño que esto sea cada vez más común y más difícil de detectar
Llegué más o menos hasta “Go is clearly working for a lot of people,” y ahí empecé a sospechar de ayuda de IA. Claro, podría no ser así, y yo tampoco soy bueno distinguiéndolo
Más que una pista concreta, irónicamente es casi una sensación. Cuando algo “suena” a texto asistido por IA, pierdo el interés enseguida aunque el texto en sí esté bien
Ojalá la gente se sintiera más cómoda escribiendo directamente sus ideas tal como se le ocurren
it's worth being precise about ...suena muchísimo más a IA que el uso de genuinePor ejemplo, este párrafo da esa impresión: “Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions.”
Cada oración dice algo, cada oración importa y cada oración cumple su función. Ese tipo de escritura uno la esperaría más de un libro o paper muy técnico que de una entrada de blog
Y justamente por eso la vuelve más difícil y más aburrida de leer
No es que espere que el texto generado por LLM no esté lleno de expresiones trilladas. Solo espero que todos mostremos mejor criterio de edición para no seguir leyendo la misma voz una y otra vez
Si es un proyecto nuevo, adelante, escríbelo en Rust
Pero si ya hay código existente y un sistema que funciona y genera ingresos, lo correcto es seguir y corregir en el lenguaje original solo las partes que realmente haya que reescribir
Mejora el sistema de forma pequeña y medible con un lenguaje que conozcas y un equipo en el que confíes. Todo lo demás es un debate religioso improductivo
Ya me gustaba Rust antes de correr benchmarks, pero la diferencia de eficiencia con la que la mayoría de los LLM escriben Rust y Go fue mucho mayor de lo que esperaba. Eso fue todavía más claro en harnesses de tipo agente que pueden arreglar problemas iniciales del entorno
Después de ver eso me volví un evangelista bastante fuerte de Rust. He tenido buenos resultados escribiendo en Rust herramientas de procesamiento por lotes para llamar desde codebases existentes, pero todavía no he intentado una migración completa de producción
Creo que los problemas de Go que menciona el texto, especialmente los relacionados con
nil, se están resolviendo cada vez más con revisiones de código muy exhaustivas hechas con Codex. Sería mejor que el problema no existiera desde el principio, pero para desarrolladores que invierten tanto esfuerzo en revisión y comprensión como en diseño e implementación, este tipo de bugs de seguridad está empezando a volverse opcionalLos datos por lenguaje están aquí: https://gertlabs.com/rankings?mode=agentic_coding
Rust te pone con fuerza sobre un camino muy definido. Codex siempre logra producir algo que compile
La desventaja es que a veces quizá debería fallar cuando un enfoque idiomático no es posible, pero en cambio puede producir una implementación tonta que compila y cumple lo pedido
Como los LLM escriben código más rápido que las personas, proporcionalmente el tiempo de espera de compilación pesa más. En proyectos de cierta escala, como de más de 100 mil líneas, la compilación de Rust, unas 10 veces más lenta, empieza a mostrarse como cuello de botella
Si vas a escribir infraestructura central quizá valga la pena pagar ese costo, pero si estás construyendo un servicio interno no expuesto a internet, la velocidad de desarrollo puede importar más
También creo que la compilación lenta afecta la velocidad de desarrollo humana, pero curiosamente muy pocos desarrolladores intentan cuantificar eso
Si la verbosidad es el principal obstáculo, esto que se espera para Go 1.28 podría reducirla bastante
https://github.com/golang/go/issues/12854#issue-110104883
Me da risa la frase “servicios de los que depende la organización, que requieren alta disponibilidad y son críticos para el negocio”
Especialmente cuando ese servicio en Rust corre sobre Kubernetes
Ya uso Rust y no tengo experiencia con Go, así que quizá este texto no esté hecho para mí
Pero sí hay algo que me hace ruido. Decir que en Rust las condiciones de carrera “se detectan en tiempo de compilación” suena, al menos, un poco exagerado
Esa frase puede dar la impresión de que Rust también puede encargarse de cosas como inanición por bloqueo mutuo u otros problemas de concurrencia. En realidad no es así
Sé que condición de carrera es un término formal con un significado más acotado, pero aun así creo que podría redactarse con más claridad