Go sigue sin ser bueno
(blog.habets.se)- Varias decisiones de diseño del lenguaje Go se tomaron de forma innecesaria o ignorando experiencia previa ya existente
- El problema de gestión del alcance de las variables de error dificulta la legibilidad del código y la búsqueda de bugs
- En varios aspectos, como la dualidad de
nil, el uso de memoria y la portabilidad del código, aparecen diseños poco intuitivos y alejados de la realidad - Las limitaciones de la sentencia
defery la forma en que la biblioteca estándar maneja situaciones excepcionales dificultan garantizar la seguridad ante excepciones - Los problemas acumulados, como la gestión de memoria y el deficiente manejo de UTF-8, están afectando negativamente a largo plazo la calidad de los codebases en Go
Crítica de largo plazo al lenguaje Go
- Como ya expuse en publicaciones anteriores (Why Go is not my favourite language, Go programs are not portable), llevo más de 10 años señalando varios problemas del lenguaje Go
- En particular, cada vez resultan más lamentables las decisiones de diseño innecesarias que ignoraron buenas prácticas ya conocidas
La falta de intuición en el alcance de las variables de error
- La sintaxis de Go amplía innecesariamente el alcance de la variable de error (
err), aumentando la posibilidad de errores- En el código de ejemplo, la variable
errpermanece viva durante toda la función y se reutiliza, lo que perjudica la legibilidad y mantenibilidad del código - Incluso desarrolladores con experiencia sufren malentendidos y pérdida de tiempo al rastrear bugs debido a estos problemas de alcance
- La sintaxis no permite limitar correctamente esas variables a un ámbito más local
- En el código de ejemplo, la variable
Dos formas de nil
- En Go existe la confusión de que
nilse comporta de manera distinta en tiposinterfacey tipos puntero- Como en el ejemplo de abajo, aunque se asigne
nilas(puntero) ei(interface),s==ise evalúa de manera distinta, mostrando un comportamiento inconsistente - Este es el tipo de problema que normalmente se quiere evitar al manejar
null, y deja ver una falta de reflexión suficiente en el diseño
- Como en el ejemplo de abajo, aunque se asigne
Límites de la portabilidad del código
- El uso de comentarios para compilación condicional es claramente ineficiente en términos de mantenibilidad y portabilidad
- Si alguna vez se ha creado software verdaderamente portable, es fácil ver que este enfoque es engorroso y propenso a errores
- Se ignoró la experiencia acumulada históricamente en portabilidad de código y casos prácticos reales
- Para más detalles, puede consultarse Go programs are not portable
La falta de claridad sobre la propiedad en append
- La relación de propiedad entre la función
appendy los slices no es clara, lo que dificulta predecir el comportamiento del código- A través del ejemplo, resulta difícil saber de antemano qué efecto real tendrá sobre el original hacer
appendal slice dentro de la funciónfoo - Aumentan así las “quirks” del lenguaje que hay que memorizar, lo que termina provocando errores
- A través del ejemplo, resulta difícil saber de antemano qué efecto real tendrá sobre el original hacer
Diseño insuficiente de la sentencia defer
- No ofrece un soporte claro para liberar recursos como sí lo hace el principio RAII (Resource Acquisition Is Initialization)
- A diferencia de las sentencias estructuradas de gestión de recursos en Java y Python, en Go no queda claro qué recursos deben liberarse con
defer - Como muestra el ejemplo con archivos, incluso hay que lidiar manualmente con problemas de double-close, y no queda claro cuál es el orden ni la forma correctos de liberar recursos
- A diferencia de las sentencias estructuradas de gestión de recursos en Java y Python, en Go no queda claro qué recursos deben liberarse con
Manejo de excepciones en la biblioteca estándar
- Go es una estructura que no soporta excepciones explícitas (
exception), pero aun así siguen ocurriendo situaciones excepcionales comopanic- En algunos casos,
panicni siquiera termina por completo el programa, sino que queda absorbido - Existen patrones en la biblioteca estándar (
fmt.Print, servidores HTTP, etc.) que ignoran excepciones, por lo que no es posible garantizar una verdadera seguridad ante excepciones - Al final, escribir código seguro ante excepciones sigue siendo indispensable, pero no se pueden usar excepciones directamente
- En algunos casos,
UTF-8 y las cadenas
- Aunque se meta cualquier dato binario arbitrario en el tipo
string, Go funciona sin hacer una validación especial- Puede ocurrir que nombres de archivo creados antes de la codificación UTF-8 simplemente se omitan silenciosamente
- En tareas como respaldos esto puede causar pérdida de datos importantes, y refleja un enfoque simplista que no toma en cuenta situaciones reales de trabajo
Límites de la gestión de memoria
- Es difícil tener control directo sobre el uso de RAM, y la confiabilidad del GC (garbage collection) también tiene límites
- El uso de memoria de Go crece y eso termina convirtiéndose a largo plazo en problemas de costo y rendimiento
- En entornos con múltiples instancias y contenedores, realmente surgen problemas de costos y escalabilidad
Conclusión: había caminos mejores
- Aunque ya existían diseños de lenguajes que habían demostrado su eficacia, Go los ignoró en muchos aspectos
- A diferencia de los problemas de los primeros borradores de Java, cuando Go fue lanzado ya existían enfoques mejores
Material de referencia
- Uber: Data race patterns in Go
- FasterThanLime: Lies we tell ourselves to keep using Golang
- FasterThanLime: I want off Mr Golang’s wild ride
1 comentarios
Comentarios de Hacker News
He usado Go desde antes de la versión 1.0 en casi todos mis trabajos de tiempo completo. Es simple para que los miembros del equipo aprendan lo básico y, en general, funciona de forma estable. Casi nunca hay de qué preocuparse al actualizar a una versión reciente de Go, y la mayoría de las funciones útiles ya vienen incluidas. La velocidad de compilación es atractiva. La concurrencia es un poco complicada, pero si le dedicas tiempo, se vuelve buena para expresar el flujo de datos. El sistema de tipos suele ser conveniente, aunque a veces resulta verboso. En general, es una herramienta confiable. Pero sí termino identificándome con varias de las críticas mencionadas en el artículo. Está claro que en Go hay partes donde desarrolladores de la vieja escuela se aferraron demasiado a los principios y dejaron pasar comodidades prácticas. Claro, esta es solo mi impresión, y también pienso que si hubieran resuelto todas las desventajas quizá hoy sería peor. También quiero mencionar que en los últimos años se siente un ambiente más abierto a corregir rarezas. Hubo una época en la que jamás habría imaginado que agregarían genéricos o iteradores personalizados. Las críticas sobre RAM y portabilidad me parecen más bien quejas personales. Estaría bien que mejorara, pero es rarísimo que el GC cause problemas graves en la mayoría de los programas, y depurar tampoco es tan difícil. Además, Go soporta casi todas las plataformas importantes. Aun así, sigo sintiéndome incómodo con la forma en que maneja errores y
nil. Extraño seguido sintaxis comoResult[Ok, Err]uOptional[T]Yo más bien creo que Go no fue terco con los principios, sino que se obsesionó con la conveniencia de resolver rápido los problemas inmediatos. No analizó el problema de fondo ni lo resolvió correctamente, sino que dio la impresión de abandonar el espíritu de “Not Invented Here” y armar cosas improvisadas sobre la marcha. El API del sistema de archivos de Go es un ejemplo representativo. Si se necesita una función para abrir archivos, simplemente hacen algo como
func Open(name string) (*File, error)y listo. Pero ¿qué pasa si el nombre del archivo no está en UTF-8? Como durante 5 años no apareció ese problema, no le prestaron atenciónMuchas veces siento que los principios de diseño de Go están demasiado concentrados en la meta de “hacer que el compilador sea fácil de construir y que compile rápido”. Es una estructura enfocada más en el compilador y la compilación que en la comodidad del desarrollador
Después de 20 años, en un trabajo nuevo empecé a usar Go en serio por primera vez como lenguaje compilado. Será cuestión de gustos, pero honestamente me llegó a generar rechazo mientras lo usaba. No hay valores por defecto para argumentos, no me gusta cómo maneja errores y no hay stack traces decentes en producción. La sintaxis orientada a objetos se ve mal porque hay que poner referencias raras en cada función. Los punteros también me pesan. Al final se siente como volver a técnicas viejas de C/C++. Es exactamente el ambiente de programación que tenía en la universidad por ahí de 1999
En cuanto a concurrencia, según mi experiencia Go es el único sistema donde el propio lenguaje maneja de forma natural el paralelismo en entornos con CPU multinúcleo. Gracias a la fórmula oficial de goroutine/channel estilo CSP, la lógica de concurrencia se expresa de forma intuitiva. Python da dolores de cabeza por el GIL y por bibliotecas
asyncdifíciles de entender. C, C++, Java y otros requieren bibliotecas adicionales fuera del lenguaje, así que no es fácil razonar sobre concurrencia a nivel del lenguaje. Por eso creo que go encaja perfectamente para servidores HTTP o servicios. En mi experiencia no hay una alternativa comparableDesde la perspectiva del desarrollador, sentí que la ergonomía, es decir, la estandarización y la consistencia, era perfecta. Incluso entre varios codebases de microservicios no hay que preocuparse por estilos distintos, ni hace falta discutir sobre formato. Eso sí, cuando Go elige su forma estándar, parece que a veces insistió demasiado en un estilo antiguo. Los desarrolladores de hoy esperan más métodos funcionales como
map/filter, pero Go solo ofrece bucles con riesgo de errores de índice. Tampoco tiene un sistema de tipos tan inteligente como TypeScript. El manejo de errores es incómodo. Entiendo la preocupación de que agregar estas funciones aumente los “usos creativos pero malos”, pero también se siente que es difícil convencer a la generación de JS de usar goLlevo más de 5 años dedicado a un proyecto grande en Golang, y cuando te toca construir componentes donde hay que minimizar al máximo el uso de memoria, te topas seguido con las partes flojas de Go. El GC no limpia lo bastante rápido o la fragmentación del heap se vuelve grave (porque Go no tiene un garbage collector compactante). Por eso tratamos de evitar por completo las asignaciones, pero eso vuelve muy fácil introducir bugs. Depurar también es extremadamente difícil. Aunque saques un perfil del heap, solo te muestra los objetos que sobrevivieron; no ves la basura acumulada real ni el detalle de la fragmentación, así que terminas adivinando. Por ejemplo, una función X puede aparecer como si solo asignara 1 KB en el heap, pero si se llama repetidamente dentro de un bucle, puede generar decenas de MB de basura. Entonces preasignamos buffers estáticos y los reutilizamos, pero eso complica los problemas de ownership y deja huecos como
append. A veces incluso hay que reimplementar la librería estándar. Sé que nuestro caso no es el más común, pero de verdad da la sensación de estar peleando contra el lenguaje, y eso decepcionaEn ese caso, puede ser menos doloroso sacar la memoria fuera del heap. Claro, al ser un lenguaje con GC no es sencillo, pero antes que forzar en Go código demasiado estilo C++/Rust, mejor cambiar esa parte directamente a uno de esos lenguajes
Creo que elegir go para una situación así fue, en sí mismo, un error de elección de lenguaje. Mi opinión es que C/C++/Rust/Zig eran más adecuados
Se comenta que el nuevo garbage collector "Green Tea" podría ayudar. No está centrado solo en memoria, pero sí usa un algoritmo de marcado paralelo que maneja mejor objetos cercanos en memoria. Se puede revisar más información aquí
Se estaba llevando a cabo el experimento de
arena, pero actualmente está suspendido. Aun así, vale la pena echarle un ojoPerdón porque esto no ayuda mucho, pero viendo la situación actual creo que la elección de lenguaje fue completamente equivocada. Supongo que quizá están usando go a la fuerza por una política interna de lenguaje oficial. Es común que las grandes empresas solo aprueben producción con lenguajes ampliamente usados
Todavía no entiendo por qué
deferen Go solo funciona a nivel de función y no de scope léxico. Me di cuenta de eso procesando archivos dentro de un bucle: cuando la lista de archivos creció,deferno cerraba los handles hasta el final de la función y eso terminó provocando un crash. Los desarrolladores de Go a mi alrededor me dijeron que envolviera el cuerpo del bucle en una función anónima. Fuera de algunos detalles menores, Go sí se siente agradable, tiene una sintaxis eficiente y además ayuda a frenar la cultura de “lucirse” innecesariamente. Reescribí a gran escala un proyecto de C# en Go y, aunque tenía solo una décima parte de las funciones, el código terminó siendo incluso más corto. En vez de forzar asignaciones de GC, orienta a usar valores predeterminados con buen rendimiento, y la generación de código integrada para tareas como serialización es cómoda. A diferencia de la sintaxis de C#, que intenta reemplazar todo con el lenguaje, en Go el ambiente parece ser: SQL se escribe como SQL, y gRPC se maneja con la especificación de protobufA veces se necesita
defera nivel de scope léxico, y a veces a nivel de función. Por ejemplo, si en un bucle quieres abrir varios archivos y mantenerlos abiertos hasta que termine la función, necesitas el scope de función. Ahora mismo es de función, pero cuando hace falta scope léxico puedes envolverlo en unafunc. Si solo soportara scope léxico y necesitaras scope de función, no estaría claro qué hacerTiene ventajas como evitar un nivel extra de indentación sin necesidad de una función envoltorio, que su comportamiento está relacionado con el call stack o el stack unwinding, y que desde la perspectiva del estilo
goto failde C resulta natural. Claro, cuando usasdeferdentro de un bucle sí es un poco incómodo tener que envolverlo aparte en una funciónHe usado tanto lenguajes con nivel de bloque como con
defera nivel de función, y a veces me gustaría poder usardefera nivel de función incluso dentro de condicionalesNo creo que haya una razón especialmente profunda, y en realidad me pregunto si de verdad importa tanto
En C# también puedes trabajar con SQL o con especificaciones protobuf. La diferencia es que hay otras opciones disponibles
Go tiene muchas desventajas, pero dentro de la categoría de lenguajes para servidor, no siento que haya otro tan equilibrado como este. Es más rápido que Node o Python, y también me parece mejor su sistema de tipos. Tiene una barrera de entrada más baja que Rust, y su librería estándar y tooling son excelentes. Me gusta su sintaxis simple y que prácticamente obligue a una sola manera de hacer las cosas. El manejo de errores sí tiene problemas, pero aun así me parece mejor que en Node, donde cualquier cosa puede caer en un
catch. Me pregunto si habrá otro lenguaje mejor que cumpla con todos esos criterios. No me considero fanático de Go; durante mi carrera he hecho mucho backend con Node, pero últimamente estoy probando Go de manera experimentalEn realidad siento que todas esas ventajas también podrían aplicarse igual a Java o C#
Me molesta un poco que se llame “Node” a un lenguaje de programación. Node es un runtime de JavaScript, y hoy en día una parte considerable de los proyectos que corren en Node están escritos en TypeScript. O sea, aunque digas Node, no queda claro cuál es el lenguaje usado. Si tomamos TypeScript como referencia, hasta siento que su sistema de tipos es más productivo que el de Go. Se podría hacer la misma afirmación frente a Rust
La mayoría de los lenguajes tienen incomodidades a su manera. Go destaca en rendimiento, portabilidad y también en runtime/ecosistema. Por otro lado, tiene desventajas como punteros
nil,zero value, falta de destructores y ausencia de macros (la carencia de macros en Go hace que se abuse de la generación de código para rodearlo). Hay lenguajes mejores (por ejemplo, Rust), pero en esos casos todo se vuelve mucho más complejo que en Go. Eso ocurre porque los creadores de Go pusieron la simplicidad como máxima prioridadConsiderando el desarrollo reciente del sistema de tipos de Python, diría que ya está muy por delante de Go. Solo comparando structural typing, Python resulta más impresionante
Creo que el sistema de tipos de Go es bastante insuficiente
Una vez extendí un static site generator hecho en Go. El código era muy claro y fácil de leer, pero por huecos del lenguaje tenía poca extensibilidad. Incluso cambios simples exigían desarmar con dificultad varias partes del código. Es complicado manejar distintos niveles de encapsulación y abstracción, y la abstracción se sacrifica en nombre de la “simplicidad”. La abstracción es la forma más importante de crear código fácil de extender, y Go elige simplicidad en lugar de extensibilidad. En general, los programas en Go se sienten como una “simplicidad sin extensibilidad”. La gente insiste en que Go simplemente es así, pero por mi experiencia eso no me convence. Al menos la “experiencia de desarrollo” no fue mala
Las conversaciones sobre Go siempre se sienten extrañas. Si lo criticas, la respuesta suele ser “ese lenguaje es así” y ya, como si hubiera que aceptarlo sin más. Dicen que la simplicidad es una fortaleza, pero me pregunto si de verdad es más simple tener que escribir tú mismo un bucle solo para sacar la lista de claves de un
mapMe gustaría preguntar si de verdad se puede lanzar una crítica así tan fácilmente después de haber usado Go solo un rato. Yo he trabajado desde 2015 con muchos codebases grandes de Go, de millones de líneas, y en varios equipos. La extensibilidad de Go no es especialmente inferior a la de C, C# o Java. Go tiende a preferir la claridad por encima de la expresividad. Por eso terminas usando menos capas de abstracción y escribiendo de forma más concreta y explícita. Pero no creo que eso implique que sea imposible de extender. El diseño modular y extensible no es algo que dé el lenguaje, sino algo que el desarrollador aprende a hacer. El código que tocaste simplemente estaba mal diseñado; no era un límite del lenguaje Go
He usado Go durante años, y aunque con cosas pequeñas puedes construir rápido, a medida que escala terminas sufriendo muchísimas pequeñas incomodidades. Depurar es especialmente una pesadilla: si hay una X sin usar (algo que pasa siempre cuando comentas temporalmente una parte mientras depuras), ni siquiera compila. También es molesto todo el formalismo innecesario, los nombres de archivo especiales y los nombres de campos reservados. Los
panicescondidos en la librería estándar y las copias inesperadas al heap también son lentas y fastidiosas. Casi toda la parte “mágica” de Go nace de reutilizar a la fuerza mecanismos ya existentes, como nombres de archivo especiales o mayúsculas/minúsculas. Si de verdad querían expresar algo como “public”, podrían haber escritopub, pero de forma extraña se mantuvieron tercos. Ahora que la IA ha mejorado tanto, cuando en Rust tengo un problema de tipos o del borrow checker, le pregunto a la IA y lo resuelvo rápido, así que termina siendo mucho más agradable. Ya no hace falta perder tiempo buscando en documentación o en SO como antesNo he entrado de lleno en Rust recientemente, pero cuando lo probé un poco en diciembre me sorprendió lo bien que la IA responde sobre Rust. Como tiene mucha sintaxis detallada e información de tipos explícita, la IA a veces lo resuelve mejor que una persona
Cuando te quejas en Go de que en depuración te topas con errores de compilación, en el mundo Go te regañan diciendo que “uses bien las herramientas”. Aplican los principios con un extremismo que incomoda
Una vez le mencioné esta incomodidad de depuración a uno de los creadores de Go, y ni siquiera entendió el problema. Me decepcionó porque me pareció demasiado amateur. Por cierto, la IA en realidad maneja peor Go. Aunque sea un lenguaje relativamente simple, ChatGPT da mejor soporte para Java, C# y Python
Personalmente no me gusta Go y le veo muchas desventajas decisivas, pero está claro por qué sigue siendo tan popular. Go es relativamente rápido y, gracias al modelo basado en goroutines, permite escribir servicios de alta concurrencia estables y confiables con facilidad sin recurrir a multithreading tradicional. Cuando Google lanzó Go, casi no existían lenguajes similares que fueran populares, estáticos y compilados. Incluso ahora, el único competidor en una posición parecida es Java (que ahora ya soporta virtual threads). Los lenguajes con async/await prometen algo parecido, pero en la práctica traen mucha complejidad (evitar bloqueos en tareas asíncronas, function coloring, etc.). Erlang también es otra categoría. Al final, pese a tantas desventajas, sigue siendo popular por las goroutines y por el peso del nombre de Google
Poco a poco la JVM está cerrando la brecha con Go. Va mejorando con proyectos como virtual threads, zgc, lilliput, Leyden y Valhalla. El cambio de Java 8 a 25 ha sido enorme. Parece que en adelante será aún más conveniente
La explicitud y simplicidad de Go son muy adecuadas para la programación asistida por LLM. Incluso código viejo de Go 1.x sigue funcionando tal cual en versiones actuales
De hecho, dentro de Google usan Java con virtual threads mucho más a menudo que Go
Me da curiosidad cuál consideran que sería el “lenguaje moderno” más adecuado para proyectos nuevos
Me gustó Go desde antes del lanzamiento 1.0, pero no comparto eso de que “todavía no lo lograron”. Claro que hay desventajas y quejas, pero también pienso que cuando los fundadores abandonan un proyecto se vuelve difícil mantener una visión central y existe el riesgo de que el lenguaje empeore. También creo que haberlo posicionado solo como “lenguaje de servidor” terminará empujando a la gente hacia Rust, Python y otros. Hubo una época en que también se burlaban de Visual Basic, pero al final quienes lo necesitaban igual lo usaban bien
Cuando se examinan con cuidado los textos críticos sobre las desventajas de Go, en realidad muchas veces no tratan temas tan graves. Casi siempre son técnicamente correctos, pero menores. En cambio, los problemas de diseño de lenguaje realmente serios serían
zero value, la falta de soporte para constructores, el mal manejo de null, la mutabilidad por defecto, un sistema de tipos que no fue pensado para genéricos,intsin precisión arbitraria yslicecon ownership ambiguo (issue relacionada 1, issue relacionada 2). También son desventajas la falta de sum types y la ausencia de interpolación de stringsPuede que esté sesgado al punto de haber escrito un libro sobre Go, pero como alguien que lo ha usado por más de 10 años, al principio de verdad me pareció muy fresco. Tiene menos boilerplate que Java, es fácil de aprender y su rendimiento es razonable. No existe el mejor lenguaje, y según el caso habrá opciones más adecuadas, pero para el trabajo típico de backend es una elección de la que no te arrepientes