Por qué cambiamos de Python a Go

Actualizado el 14 de mayo de 2019 para reflejar mejor las mejoras de Go en los últimos 2 años (gestión de paquetes, mejor rendimiento, tiempos de compilación más rápidos y un ecosistema más maduro) Cambiar a un nuevo lenguaje siempre es un gran paso, especialmente cuando solo uno de los miembros de tu equipo tiene experiencia previa con ese lenguaje. A principios de este año, cambiamos el lenguaje de programación principal de Stream de Python a Go. Este artículo explicará algunas de las razones por las que decidimos dejar atrás Python y hacer el cambio a Go. Gracias a Ren Sakamoto por traducir al japonés Por qué cambiamos de Python a Go, なぜ私達は Python から Go に移行したのか.

Razón 1 – Rendimiento

¡Go es rápido! Go es extremadamente rápido. El rendimiento es similar al de Java o C++. Para nuestro caso de uso, Go es típicamente 40 veces más rápido que Python. Aquí hay un pequeño juego de referencia comparando Go vs Python.

Razón 2 – El rendimiento del lenguaje importa

Para muchas aplicaciones, el lenguaje de programación es simplemente el pegamento entre la aplicación y la base de datos. El rendimiento del lenguaje en sí no suele importar mucho. Stream, sin embargo, es un proveedor de API que impulsa una plataforma de feeds y chat para 700 empresas y más de 500 millones de usuarios finales. Llevamos años optimizando Cassandra, PostgreSQL, Redis, etc., pero al final llegas a los límites del lenguaje que utilizas. Python es un gran lenguaje, pero su rendimiento es bastante lento para casos de uso como la serialización/deserialización, la clasificación y la agregación. Frecuentemente nos encontramos con problemas de rendimiento en los que Cassandra tardaba 1ms en recuperar los datos y Python se pasaba los siguientes 10ms convirtiéndolos en objetos.

Razón 3 – Productividad del desarrollador &No ser demasiado creativo

Echa un vistazo a este pequeño fragmento de código Go del tutorial How I Start Go. (Este es un gran tutorial y un buen punto de partida para recoger un poco de Go.)

Si eres nuevo en Go, no hay mucho que te sorprenda al leer ese pequeño fragmento de código. Muestra múltiples asignaciones, estructuras de datos, punteros, formato y una biblioteca HTTP incorporada. Cuando empecé a programar siempre me gustó usar las características más avanzadas de Python. Python te permite ser bastante creativo con el código que escribes. Por ejemplo, puedes:

  • Usar MetaClasses para autorregistrar clases al inicializar el código
  • Intercambiar True y False
  • Añadir funciones a la lista de funciones incorporadas
  • Sobrecargar operadores a través de métodos mágicos
  • Usar funciones como propiedades a través del decorador @property

Estas características son divertidas para jugar pero, como la mayoría de los programadores estarán de acuerdo, a menudo hacen que el código sea más difícil de entender cuando se lee el trabajo de otra persona. Go te obliga a ceñirte a lo básico. Esto hace que sea muy fácil leer el código de cualquier persona y entender inmediatamente lo que está pasando. Nota: Lo «fácil» que es realmente depende de tu caso de uso, por supuesto. Si quieres crear una API CRUD básica yo seguiría recomendando Django + DRF, o Rails.

Razón 4 – Concurrencia & Canales

Como lenguaje, Go trata de mantener las cosas simples. No introduce muchos conceptos nuevos. Se centra en crear un lenguaje sencillo que sea increíblemente rápido y fácil de trabajar. La única área en la que se innova son las goroutines y los canales. (Para ser 100% correctos, el concepto de CSP comenzó en 1977, así que esta innovación es más bien un nuevo enfoque de una vieja idea). Las goroutines son la aproximación ligera de Go a los hilos, y los canales son la forma preferida de comunicarse entre goroutines. Las goroutines son muy baratas de crear y sólo ocupan unos pocos KBs de memoria adicional. Como las goroutines son tan ligeras, es posible tener cientos o incluso miles de ellas ejecutándose al mismo tiempo. Puedes comunicarte entre goroutines usando canales. El tiempo de ejecución de Go maneja toda la complejidad. Las goroutines y el enfoque basado en canales para la concurrencia hace que sea muy fácil utilizar todos los núcleos de CPU disponibles y manejar IO concurrente – todo sin complicar el desarrollo. En comparación con Python/Java, la ejecución de una función en una goroutine requiere un mínimo de código de caldera. Simplemente hay que anteponer a la llamada a la función la palabra clave «go»:

https://tour.golang.org/concurrency/1 La aproximación de Go a la concurrencia es muy fácil de trabajar. Es un enfoque interesante en comparación con Node, donde el desarrollador tiene que prestar mucha atención a cómo se maneja el código asíncrono. Otro gran aspecto de la concurrencia en Go es el detector de carreras. Esto hace que sea fácil de averiguar si hay alguna condición de carrera dentro de su código asíncrono.

Knock knock Race condition ¿Quién está ahí?

– I Am Devloper (@iamdevloper) November 11, 2013

Aquí tienes unos cuantos buenos recursos para empezar con Go y los canales:

  • https://gobyexample.com/channels
  • https://tour.golang.org/concurrency/2
  • http://guzalexander.com/2013/12/06/golang-channels-tutorial.html
  • https://www.golang-book.com/books/intro/10
  • https://www.goinggo.net/2014/02/the-nature-of-channels-in-go.html
  • Goroutines vs Green threads

Razón 5 – Tiempo de compilación rápido

Nuestro mayor microservicio escrito en Go actualmente tarda 4 segundos en compilar. Los rápidos tiempos de compilación de Go son una gran victoria de productividad en comparación con lenguajes como Java y C++, que son famosos por su lenta velocidad de compilación. Me gusta la lucha de espadas, pero es aún más agradable hacer las cosas mientras todavía recuerdo lo que el código se supone que debe hacer:

Razón 6 – La capacidad de construir un equipo

En primer lugar, vamos a empezar con lo obvio: no hay tantos desarrolladores de Go en comparación con lenguajes más antiguos como C++ y Java. Según StackOverflow, el 38% de los desarrolladores conoce Java, el 19,3% conoce C++ y sólo el 4,6% conoce Go. Los datos de GitHub muestran una tendencia similar: Go es más utilizado que lenguajes como Erlang, Scala y Elixir, pero menos popular que Java y C++. Afortunadamente, Go es un lenguaje muy sencillo y fácil de aprender. Proporciona las características básicas que necesitas y nada más. Los nuevos conceptos que introduce son la sentencia «defer» y la gestión integrada de la concurrencia con «rutinas go» y canales. (Para los puristas: Go no es el primer lenguaje que implementa estos conceptos, sólo el primero que los hace populares). Cualquier desarrollador de Python, Elixir, C++, Scala o Java que se una a un equipo puede ser eficaz en Go en un mes debido a su simplicidad. Hemos encontrado que es más fácil construir un equipo de desarrolladores de Go en comparación con muchos otros lenguajes. Si estás contratando gente en ecosistemas competitivos como Boulder y Ámsterdam esto es un beneficio importante.

Razón 7 – Ecosistema fuerte

Para un equipo de nuestro tamaño (~20 personas) el ecosistema importa. Simplemente no puedes crear valor para tus clientes si tienes que reinventar cada pequeña pieza de funcionalidad. Go tiene un gran soporte para las herramientas que utilizamos. Ya había bibliotecas sólidas para Redis, RabbitMQ, PostgreSQL, análisis de plantillas, programación de tareas, análisis de expresiones y RocksDB. El ecosistema de Go es una gran victoria en comparación con otros lenguajes más nuevos como Rust o Elixir. Por supuesto no es tan bueno como lenguajes como Java, Python o Node, pero es sólido y para muchas necesidades básicas encontrarás paquetes de alta calidad ya disponibles.

Razón 8 – Gofmt, Enforced Code Formatting

Empecemos por lo que es Gofmt? Y no, no es una palabrota. Gofmt es una impresionante utilidad de línea de comandos, integrada en el compilador Go para formatear tu código. En términos de funcionalidad es muy similar al autopep8 de Python. Aunque el programa Silicon Valley retrata lo contrario, a la mayoría de nosotros no nos gusta discutir sobre tabulaciones o espacios. Es importante que el formato sea consistente, pero el estándar de formato real no importa mucho. Gofmt evita toda esta discusión al tener una forma oficial de formatear su código.

Razón 9 – gRPC y Buffers de Protocolo

Go tiene soporte de primera clase para los buffers de protocolo y gRPC. Estas dos herramientas funcionan muy bien juntas para construir microservicios que necesitan comunicarse vía RPC. Sólo hay que escribir un manifiesto donde se definen las llamadas RPC que se pueden hacer y qué argumentos toman. Tanto el código del servidor como el del cliente se generan automáticamente a partir de este manifiesto. Este código resultante es rápido, tiene una huella de red muy pequeña y es fácil de usar. A partir del mismo manifiesto, se puede generar código de cliente para muchos lenguajes diferentes incluso, como C++, Java, Python y Ruby. Por lo tanto, se acabaron los puntos finales REST ambiguos para el tráfico interno, para los que hay que escribir casi el mismo código de cliente y servidor cada vez.

Desventaja 1 – Falta de Frameworks

Go no tiene un único framework dominante como Rails para Ruby, Django para Python o Laravel para PHP. Este es un tema de acalorado debate dentro de la comunidad de Go, ya que mucha gente defiende que no se debe usar un framework para empezar. Estoy totalmente de acuerdo en que esto es cierto para algunos casos de uso. Sin embargo, si alguien quiere construir una simple API CRUD lo tendrá mucho más fácil con Django/DJRF, Rails Laravel o Phoenix. Actualización: como se ha señalado en los comentarios, hay varios proyectos que proporcionan un marco de trabajo para Go. Revel, Iris, Echo, Macaron y Buffalo parecen ser los principales contendientes. Para el caso de uso de Stream preferimos no usar un framework. Sin embargo, para muchos proyectos nuevos que buscan proporcionar una simple API CRUD la falta de un marco dominante será una seria desventaja.

Desventaja 2 – Manejo de errores

Go maneja los errores simplemente devolviendo un error desde una función y esperando que su código de llamada maneje el error (o lo devuelva a la pila de llamadas). Mientras que este enfoque funciona, es fácil perder el alcance de lo que salió mal para asegurarse de que puede proporcionar un error significativo a sus usuarios. El paquete de errores resuelve este problema permitiéndote añadir contexto y un seguimiento de la pila a tus errores. Otro problema es que es fácil olvidar manejar un error por accidente. Las herramientas de análisis estático como errcheck y megacheck son útiles para evitar cometer estos errores. Mientras que estas soluciones funcionan bien, no se sienten del todo bien. Uno esperaría que el manejo de errores adecuado fuera soportado por el lenguaje.

Desventaja 3 – Gestión de paquetes

Actualización: La gestión de paquetes de Go ha recorrido un largo camino desde que se escribió este post. Los módulos de Go son una solución efectiva, el único problema que he visto con ellos es que rompen algunas herramientas de análisis estático como errcheck. Aquí hay un tutorial para aprender a usar Go usando módulos Go. La gestión de paquetes de Go no es en absoluto perfecta. Por defecto, no tiene una manera de especificar una versión específica de una dependencia y no hay manera de crear construcciones reproducibles. Python, Node y Ruby tienen mejores sistemas de gestión de paquetes. Sin embargo, con las herramientas adecuadas, la gestión de paquetes de Go funciona bastante bien. Puedes usar Dep para gestionar tus dependencias y permitir especificar y anclar versiones. Aparte de eso, hemos contribuido con una herramienta de código abierto llamada VirtualGo que facilita el trabajo en múltiples proyectos escritos en Go.

Python vs Go

Actualización: La diferencia de rendimiento entre Python y Go aumentó desde que se escribió este post. (Go se volvió más rápido y Python no) Un experimento interesante que realizamos fue tomar nuestra funcionalidad de alimentación clasificada en Python y reescribirla en Go. Echa un vistazo a este ejemplo de un método de clasificación:

Tanto el código de Python como el de Go necesitan hacer lo siguiente para soportar este método de clasificación:

  1. Parar la expresión para la puntuación. En este caso, queremos convertir esta cadena «simple_gauss(tiempo)*popularidad» en una función que toma una actividad como entrada y devuelve una puntuación como salida.
  2. Crear funciones parciales basadas en la configuración JSON. Por ejemplo, queremos que «simple_gauss» llame a «decay_gauss» con una escala de 5 días, un desplazamiento de 1 día y un factor de decaimiento de 0,3.
  3. Particula la configuración «por defecto» para que tengas un recurso si un determinado campo no está definido en una actividad.
  4. Usa la función del paso 1 para puntuar todas las actividades en el feed.

Desarrollar la versión Python del código de clasificación tomó aproximadamente 3 días. Eso incluye escribir el código, las pruebas unitarias y la documentación. Luego, hemos pasado aproximadamente 2 semanas optimizando el código. Una de las optimizaciones fue traducir la expresión de puntuación (simple_gauss(tiempo)*popularidad) en un árbol de sintaxis abstracto. También hemos implementado una lógica de caché que precalcula la puntuación para determinados momentos en el futuro. En cambio, el desarrollo de la versión Go de este código llevó aproximadamente 4 días. El rendimiento no requirió ninguna optimización adicional. Así que, aunque el desarrollo inicial fue más rápido en Python, la versión basada en Go requirió finalmente mucho menos trabajo por parte de nuestro equipo. Como beneficio adicional, el código Go funcionó aproximadamente 40 veces más rápido que nuestro código Python altamente optimizado. Éste es sólo un ejemplo de las mejoras de rendimiento que hemos experimentado al cambiar a Go. Es, por supuesto, comparar manzanas con naranjas:

  • El código de clasificación fue mi primer proyecto en Go
  • El código Go se construyó después del código Python, por lo que el caso de uso fue mejor entendido
  • La biblioteca Go para el análisis sintáctico de expresiones era de una calidad excepcional

Su kilometraje variará. Algunos otros componentes de nuestro sistema tardaron bastante más tiempo en construirse en Go en comparación con Python. Como tendencia general, vemos que desarrollar código en Go requiere un poco más de esfuerzo. Sin embargo, pasamos mucho menos tiempo optimizando el código para el rendimiento.

Elixir vs Go – El subcampeón

Otro lenguaje que evaluamos es Elixir. Elixir está construido sobre la máquina virtual Erlang. Es un lenguaje fascinante y lo consideramos ya que uno de los miembros de nuestro equipo tiene mucha experiencia con Erlang. Para nuestros casos de uso, nos dimos cuenta de que el rendimiento bruto de Go es mucho mejor. Tanto Go como Elixir harán un gran trabajo sirviendo miles de peticiones concurrentes. Sin embargo, si se observa el rendimiento de las solicitudes individuales, Go es sustancialmente más rápido para nuestro caso de uso. Otra razón por la que elegimos Go en lugar de Elixir fue el ecosistema. Para los componentes que necesitábamos, Go tenía bibliotecas más maduras mientras que, en muchos casos, las bibliotecas de Elixir no estaban preparadas para su uso en producción. También es más difícil formar/encontrar desarrolladores que trabajen con Elixir. Estas razones inclinaron la balanza a favor de Go. Sin embargo, el framework Phoenix para Elixir parece impresionante y definitivamente vale la pena echarle un vistazo.

Conclusión

Go es un lenguaje muy performante con gran soporte para la concurrencia. Es casi tan rápido como lenguajes como C++ y Java. Mientras que toma un poco más de tiempo para construir cosas usando Go en comparación con Python o Ruby, se ahorrará una tonelada de tiempo dedicado a la optimización del código. Tenemos un pequeño equipo de desarrollo en Stream que alimenta feeds y chat para más de 500 millones de usuarios finales. La combinación de Go de un gran ecosistema, la facilidad de incorporación para los nuevos desarrolladores, el rápido rendimiento, el sólido soporte para la concurrencia y un entorno de programación productivo lo convierten en una gran elección. Stream sigue utilizando Python para nuestro panel de control, el sitio y el aprendizaje automático para los feeds personalizados. No nos despediremos de Python a corto plazo, pero a partir de ahora todo el código de alto rendimiento se escribirá en Go. Nuestra nueva API de chat también está escrita completamente en Go. Si quieres aprender más sobre Go, echa un vistazo a las publicaciones del blog que aparecen a continuación. Para aprender más sobre Stream, este tutorial interactivo es un gran punto de partida.

Más lecturas sobre el cambio a Golang

  • https://movio.co/en/blog/migrate-Scala-to-Go/
  • https://hackernoon.com/why-i-love-golang-90085898b4f7
  • https://sendgrid.com/blog/convince-company-go-golang/
  • https://dave.cheney.net/2017/03/20/why-go

Aprendiendo Go

  • https://learnxinyminutes.com/docs/go/
  • https://tour.golang.org/
  • http://howistart.org/posts/go/1/
  • https://getstream.io/blog/building-a-performant-api-using-go-and-cassandra/
  • https://www.amazon.com/gp/product/0134190440
  • Tutorial de Go Rocket

Deja una respuesta

Tu dirección de correo electrónico no será publicada.