Go vs Python: cuándo elegir rendimiento y cuándo elegir velocidad de desarrollo
Comparación práctica entre Go y Python para backend, APIs, concurrencia, automatización y despliegue. Sin fanatismos, con criterio técnico real.

Llevo años escribiendo Python a diario. Scripts de automatización, pipelines de datos, APIs internas con FastAPI, scrapers, herramientas de línea de comandos. Python es mi navaja suiza y no pienso dejarlo. Pero hace un tiempo empecé a explorar Go para servicios backend y hay cosas que me han sorprendido. No porque Go sea “mejor” que Python. Sino porque resuelve ciertos problemas de una forma que Python, por diseño, no puede.
Este artículo no es una tabla de features para que elijas un bando. Es lo que he aprendido trabajando con ambos lenguajes en contextos reales: dónde cada uno brilla, dónde sufre y cuándo merece la pena considerar el cambio. Si vienes de Python y estás pensando en aprender Go, aquí vas a encontrar criterio para decidir si te compensa.
Dos filosofías opuestas que funcionan
Python y Go nacieron con objetivos diferentes. Entender esto es clave para no compararlos donde no toca.
Python sigue la filosofía de “batteries included”. Quieres hacer scraping, tienes BeautifulSoup y Scrapy. Quieres una API, tienes FastAPI y Flask. Quieres machine learning, tienes scikit-learn, PyTorch y todo el ecosistema. Python confía en que la productividad del desarrollador es lo primero y acepta que eso tiene un coste en rendimiento.
Go hace exactamente lo contrario. Su filosofía es la simplicidad deliberada. Pocas formas de hacer cada cosa. Sin herencia, sin excepciones, sin generics hasta hace poco. Un sistema de tipos estático que te obliga a ser explícito. La librería estándar es potente pero contenida. Go confía en que un código simple y predecible escala mejor que un código expresivo pero impredecible.
La diferencia fundamental: Python optimiza para el tiempo del desarrollador al escribir el código. Go optimiza para el tiempo del equipo al mantenerlo.
Esto no es teoría. Lo notas en el día a día. En Python puedes resolver un problema en diez líneas con list comprehensions anidadas y un par de lambdas. Elegante, compacto, Pythonic. En Go, el mismo problema te va a costar treinta líneas con bucles for explícitos y manejo de errores en cada paso. Más verboso, sí. Pero cualquier persona del equipo va a entender ese código en cinco segundos sin necesidad de desenrollar mentalmente la abstracción.
Ninguna de las dos aproximaciones es mejor. Depende del problema.
Backend APIs: FastAPI vs Go net/http
Esta es la comparación más directa y donde la mayoría de gente empieza a plantearse Go. Veamos un endpoint simple que devuelve un usuario por ID.
Python con FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
id: int
name: str
email: str
users_db: dict[int, User] = {
1: User(id=1, name="Roger", email="roger@example.com"),
}
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
user = users_db.get(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return userArrancas con uvicorn main:app --reload, tienes docs automáticas en /docs, validación de tipos integrada. Productividad brutal.
Go con net/http (librería estándar)
package main
import (
"encoding/json"
"net/http"
"strconv"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var usersDB = map[int]User{
1: {ID: 1, Name: "Roger", Email: "roger@example.com"},
}
func getUser(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("user_id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
user, exists := usersDB[id]
if !exists {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{user_id}", getUser)
http.ListenAndServe(":8080", mux)
}Más código, sí. Pero fíjate en lo que obtienes: sin dependencias externas, un binario compilado, tipado estático real y control total sobre la respuesta HTTP. No necesitas framework, ni servidor ASGI, ni nada más.
Si quieres algo más cercano a la experiencia de FastAPI, puedes usar Gin y la cosa se simplifica:
func main() {
r := gin.Default()
r.GET("/users/:user_id", func(c *gin.Context) {
id, err := strconv.Atoi(c.Param("user_id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
user, exists := usersDB[id]
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
c.JSON(http.StatusOK, user)
})
r.Run(":8080")
}Veredicto para APIs
| Aspecto | Python (FastAPI) | Go (net/http / Gin) |
|---|---|---|
| Tiempo hasta primer endpoint | Minutos | Minutos |
| Documentación automática | Sí (OpenAPI integrado) | No (necesitas herramientas extra) |
| Validación de entrada | Pydantic integrado | Manual o con binding de Gin |
| Rendimiento bajo carga | Bueno (async) | Excelente |
| Dependencias necesarias | uvicorn + FastAPI + Pydantic | Ninguna (librería estándar) |
| Curva de aprendizaje | Baja | Media |
Para APIs internas, prototipos o servicios que no van a recibir miles de peticiones por segundo, FastAPI es difícil de superar. Para servicios de producción con alta concurrencia y requisitos de latencia, Go tiene ventaja real. Si quieres profundizar, tengo un artículo dedicado a montar una API REST con Go desde cero.
Concurrencia: donde Go marca la diferencia
Aquí es donde la conversación se pone interesante de verdad. Y donde Python tiene una limitación estructural que no se resuelve con librerías.
El problema del GIL en Python
Python tiene el Global Interpreter Lock (GIL). Esto significa que, aunque uses threads, solo un thread ejecuta código Python a la vez. Para I/O (peticiones HTTP, base de datos, ficheros) no importa mucho porque los threads liberan el GIL mientras esperan. Pero para CPU (procesamiento de datos, cálculos) los threads de Python no te dan paralelismo real.
Python tiene asyncio para concurrencia cooperativa y multiprocessing para paralelismo real, pero cada opción viene con sus compromisos:
import asyncio
import httpx
async def fetch_url(client: httpx.AsyncClient, url: str) -> str:
response = await client.get(url)
return response.text
async def main():
urls = [f"https://httpbin.org/delay/1" for _ in range(10)]
async with httpx.AsyncClient() as client:
tasks = [fetch_url(client, url) for url in urls]
results = await asyncio.gather(*tasks)
print(f"Obtenidos {len(results)} resultados")
asyncio.run(main())Funciona bien para I/O concurrente. Pero async/await es contagioso: una vez que entras en el mundo async, todo tu código tiene que ser async. Y si necesitas paralelismo de CPU, necesitas multiprocessing, que crea procesos separados con su propia memoria y la complejidad que eso implica.
Goroutines: concurrencia como ciudadano de primera clase
Go no tiene este problema. Las goroutines son ligeras (unos pocos KB de stack), las gestiona el runtime de Go (no el sistema operativo) y puedes lanzar miles sin pestañear:
package main
import (
"fmt"
"io"
"net/http"
"sync"
)
func fetchURL(url string, wg *sync.WaitGroup, results chan<- string) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
results <- fmt.Sprintf("Error: %v", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
results <- fmt.Sprintf("OK: %d bytes", len(body))
}
func main() {
urls := make([]string, 10)
for i := range urls {
urls[i] = "https://httpbin.org/delay/1"
}
var wg sync.WaitGroup
results := make(chan string, len(urls))
for _, url := range urls {
wg.Add(1)
go fetchURL(url, &wg, results)
}
go func() {
wg.Wait()
close(results)
}()
for result := range results {
fmt.Println(result)
}
}La diferencia clave: en Go la concurrencia es parte del lenguaje, no un añadido. No hay “modo async” y “modo sync”. Todo el código funciona igual. Lanzar una goroutine es tan natural como llamar a una función con go delante. Y los channels te dan un mecanismo elegante para comunicar goroutines sin compartir memoria directamente.
Si tu servicio necesita gestionar muchas conexiones simultáneas, procesar tareas en paralelo o coordinar workers, Go te da herramientas que en Python requieren bastante más esfuerzo y cuidado.
Para un ejemplo práctico de concurrencia en Go, tengo un artículo donde entro más en detalle con goroutines, channels y patrones reales.
Rendimiento: dónde importa y dónde no
Decir que “Go es más rápido que Python” es cierto pero incompleto. La pregunta correcta es: donde tu código necesita ir rápido, cuánto más rápido va Go?
Números reales
En benchmarks típicos de procesamiento CPU, Go es entre 10x y 40x más rápido que Python puro. No es una diferencia marginal. Es la diferencia entre que un proceso tarde 2 segundos o 60.
Ejemplo simple: contar los números primos hasta un millón.
def count_primes(limit: int) -> int:
count = 0
for n in range(2, limit):
is_prime = True
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
is_prime = False
break
if is_prime:
count += 1
return count
# En mi máquina: ~4.2 segundos para limit=1_000_000func countPrimes(limit int) int {
count := 0
for n := 2; n < limit; n++ {
isPrime := true
for i := 2; i*i <= n; i++ {
if n%i == 0 {
isPrime = false
break
}
}
if isPrime {
count++
}
}
return count
}
// En mi máquina: ~0.15 segundos para limit=1_000_000Eso es ~28x más rápido. Y Go ni siquiera está usando goroutines aquí. Con paralelismo, la diferencia sería mayor.
Pero no todo es CPU
La mayoría de aplicaciones backend pasan más tiempo esperando I/O (base de datos, APIs externas, disco) que procesando CPU. En esos casos la diferencia de rendimiento es mucho menor porque el cuello de botella no es el lenguaje, sino la red o el disco.
| Escenario | Diferencia Go vs Python | Importa? |
|---|---|---|
| Cálculos CPU intensivos | 10x-40x | Mucho |
| Procesamiento de datos en memoria | 5x-20x | Bastante |
| API REST típica (CRUD + DB) | 2x-5x | Depende del volumen |
| Script que llama APIs externas | Marginal | Poco |
| Automatización I/O bound | Marginal | No |
Si tu servicio gestiona 50 peticiones por minuto, Python va sobrado. Si gestiona 5.000 por segundo con requisitos de latencia baja, Go te da margen que en Python tendrías que compensar con más instancias, más infraestructura y más complejidad operativa.
Para entender mejor este tema con datos concretos, mira lo que explico sobre Go para tareas pesadas.
Despliegue: el binario único cambia las reglas
Esto es algo que no se aprecia hasta que lo vives en producción. Desplegar Python y desplegar Go son experiencias radicalmente diferentes.
Desplegar Python
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]La imagen resultante: entre 200MB y 500MB dependiendo de las dependencias. Necesitas gestionar requirements.txt o pyproject.toml, entornos virtuales, versiones de Python. Si usas librerías con extensiones C (numpy, pandas, lxml), la imagen se complica con dependencias de sistema operativo. Gestionar versiones de Python en el equipo es otro dolor: pyenv, venv, poetry, uv, cada proyecto con su propio ritual.
Desplegar Go
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]La imagen resultante: entre 5MB y 20MB. Un binario estático sin dependencias de runtime. Puedes usar scratch o distroless como imagen base porque no necesitas ni sistema operativo. No hay versión de Go que gestionar en producción, no hay dependencias de sistema, no hay entorno virtual.
Un binario Go compilado es un fichero que copias y ejecutas. Sin runtime, sin intérprete, sin virtualenv. La simplicidad operativa es brutal.
| Aspecto | Python | Go |
|---|---|---|
| Tamaño de imagen Docker | 200-500 MB | 5-20 MB |
| Dependencias de runtime | Python + pip + libs | Ninguna |
| Tiempo de arranque | 1-3 segundos | Milisegundos |
| Gestión de versiones | pyenv/venv/poetry/uv | go.mod (incluido en el lenguaje) |
| Cross-compilation | Complejo | GOOS=linux GOARCH=amd64 go build |
Para APIs y microservicios en producción, la diferencia operativa es significativa. Menos superficie de ataque, menos cosas que pueden fallar, despliegues más rápidos.
Data, IA y Machine Learning: Python gana por goleada
Aquí no hay debate. Si trabajas con datos, machine learning o inteligencia artificial, Python es el estándar de la industria y Go no compite.
El ecosistema de Python para data es inmenso:
- Análisis de datos: pandas, polars, NumPy
- Machine learning: scikit-learn, XGBoost, LightGBM
- Deep learning: PyTorch, TensorFlow, JAX
- NLP: spaCy, Hugging Face Transformers
- Visualización: matplotlib, seaborn, plotly
- Notebooks: Jupyter, que es insustituible para exploración
Go tiene algunas librerías para machine learning, pero son marginales comparadas con el ecosistema Python. No tiene nada comparable a pandas para manipulación de datos. No tiene frameworks de deep learning serios. Y los notebooks no existen en Go.
Si tu trabajo implica entrenar modelos, analizar datasets, construir pipelines de datos o cualquier cosa relacionada con IA, Python es la opción correcta sin discusión. Go puede complementar como servicio que sirve el modelo entrenado (inferencia en producción), pero el desarrollo del modelo siempre va a ser en Python.
Scripting y automatización: Python es más práctico
Para scripts puntuales, automatizaciones y herramientas rápidas, Python sigue siendo mi primera opción. La razón es simple: la fricción de escribir un script en Python es mínima.
# Renombrar ficheros en un directorio con un patrón
from pathlib import Path
source = Path("./exports")
for f in source.glob("*.csv"):
new_name = f.stem.replace(" ", "_").lower() + f.suffix
f.rename(f.parent / new_name)
print(f"Renamed: {f.name} -> {new_name}")El equivalente en Go:
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func main() {
source := "./exports"
entries, err := os.ReadDir(source)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
for _, entry := range entries {
if entry.IsDir() || filepath.Ext(entry.Name()) != ".csv" {
continue
}
oldPath := filepath.Join(source, entry.Name())
name := strings.TrimSuffix(entry.Name(), ".csv")
newName := strings.ToLower(strings.ReplaceAll(name, " ", "_")) + ".csv"
newPath := filepath.Join(source, newName)
if err := os.Rename(oldPath, newPath); err != nil {
fmt.Fprintf(os.Stderr, "Error renaming %s: %v\n", entry.Name(), err)
continue
}
fmt.Printf("Renamed: %s -> %s\n", entry.Name(), newName)
}
}Funcional, correcto, robusto. Pero para un script puntual, la versión Python es más rápida de escribir y más fácil de modificar. No necesitas compilar, no necesitas declarar tipos, no necesitas manejar errores explícitamente si no te importa que falle ruidosamente.
Go tiene sentido para herramientas CLI que vas a distribuir o mantener. Para el script que ejecutas una vez y tiras, Python es más eficiente en tu tiempo.
Manejo de errores: filosofías que condicionan el código
Un punto que merece mención aparte es cómo cada lenguaje gestiona los errores, porque afecta directamente a la experiencia de desarrollo.
Python usa excepciones. Puedes ignorar errores hasta que explotan en producción:
def get_user_email(user_id: int) -> str:
user = db.get_user(user_id) # puede lanzar ConnectionError
return user["email"] # puede lanzar KeyErrorFunciona. Pero si db.get_user falla o el usuario no tiene email, tienes una excepción no manejada. Puedes meter try/except, pero el lenguaje no te obliga.
Go te fuerza a manejar cada error explícitamente:
func getUserEmail(userID int) (string, error) {
user, err := db.GetUser(userID)
if err != nil {
return "", fmt.Errorf("fetching user %d: %w", userID, err)
}
if user.Email == "" {
return "", fmt.Errorf("user %d has no email", userID)
}
return user.Email, nil
}Sí, es más verboso. Y sí, el patrón if err != nil se repite constantemente. Pero cada camino de error está documentado en el código. No hay sorpresas en producción por una excepción que nadie capturó. Cuando mantienes un servicio que procesa millones de peticiones, esa previsibilidad vale su peso en oro.
En Python confías en que alguien puso el try/except donde hacía falta. En Go, el compilador no te deja ignorar un error. Son dos contratos diferentes con el desarrollador.
Cuándo elegir cada uno: matriz de decisión
Después de trabajar con ambos, mi criterio se reduce a esto:
Elige Python cuando:
- Prototipado rápido: necesitas validar una idea en horas, no en días
- Data science y ML: no hay alternativa real
- Automatizaciones y scripts: la fricción es mínima
- APIs internas: pocas peticiones, equipo que ya sabe Python
- Integración con ecosistema de datos: pandas, notebooks, pipelines
- El equipo es de Python: la productividad del equipo pesa más que el rendimiento teórico
Elige Go cuando:
- Servicios de alta concurrencia: muchas conexiones simultáneas, websockets, streaming
- Microservicios en producción: donde la latencia y el consumo de recursos importan
- CLIs y herramientas distribuibles: un binario que funciona en cualquier sitio
- Infraestructura y cloud native: Kubernetes, Docker, Terraform están escritos en Go por algo
- El despliegue importa: imágenes pequeñas, arranque rápido, sin dependencias de runtime
- Procesamiento CPU intensivo: compilado siempre gana a interpretado
Y la zona gris
Hay casos donde ambos sirven. Una API REST estándar con base de datos, un servicio de procesamiento de colas, un worker que consume de Kafka. En esos casos, mi consejo: elige el que domine mejor tu equipo. Un servicio bien escrito en Python rinde mejor que uno mal escrito en Go.
| Caso de uso | Recomendación | Motivo |
|---|---|---|
| API interna / prototipo | Python (FastAPI) | Velocidad de desarrollo |
| API producción alto tráfico | Go | Rendimiento + despliegue |
| Scripts y automatización | Python | Menos fricción |
| Data pipeline / ETL | Python | Ecosistema |
| Machine learning | Python | Sin alternativa |
| CLI distribuible | Go | Binario único |
| Microservicio cloud native | Go | Imagen ligera, arranque rápido |
| Worker/procesador de colas | Ambos | Depende del equipo |
| WebSocket / streaming | Go | Goroutines |
Conclusión: se complementan, no se reemplazan
Después de meses explorando Go viniendo de Python, mi conclusión es que no son lenguajes que compitan. Ocupan nichos diferentes y lo hacen bien.
Python sigue siendo mi herramienta principal para automatizaciones, scripts, datos y cualquier cosa que necesite iterar rápido. No voy a dejar de usarlo. Su ecosistema para data science y machine learning es insustituible. Su velocidad de desarrollo para prototipos y herramientas internas sigue sin rival.
Go ha pasado a ser mi opción preferida para servicios backend que van a producción con requisitos de rendimiento, servicios que necesitan gestionar concurrencia de forma predecible y herramientas que quiero distribuir como un binario sin dependencias. La simplicidad del despliegue y la previsibilidad del código compilado con tipos estáticos me dan confianza en producción.
La clave no es elegir uno y descartar el otro. Es saber qué problema tienes delante y elegir la herramienta que mejor lo resuelve. Si tu equipo es fuerte en Python y el servicio no tiene requisitos extremos de rendimiento, Python va sobrado. Si necesitas un servicio concurrente, ligero y fácil de desplegar, Go merece que le dediques tiempo.
Si estás considerando dar el salto, empieza por aprender Go con un proyecto pequeño. Un CLI, un worker, una API sencilla. No intentes reescribir tu monolito de Python en Go el primer día. Ve probando, ve formando criterio. Y quédate con lo que funcione para tu contexto.


