- Go es una opción para reducir la complejidad excesiva del desarrollo backend, y sus ventajas clave son la compilación rápida, el despliegue como binario único y una gestión estable de dependencias.
- En lugar de abstracciones complejas como decoradores, metaclases, macros, traits o mónadas, Go elige un diseño de lenguaje simple centrado en structs, funciones, interfaces, goroutines y channels.
- Solo con la biblioteca estándar y herramientas básicas como
embed, html/template, net/http, database/sql, encoding/json, go test y pprof, es posible cubrir apps web, bases de datos, pruebas, benchmarks y profiling.
- Las goroutines son unidades de ejecución con stack administrado de aproximadamente 2 KB de costo, y permiten manejar concurrencia y propagación de cancelación de forma simple mediante channels,
sync.Mutex, el race detector y context.Context.
- El flujo
go mod init, go build, scp, systemctl restart favorece despliegues simples basados en un único binario de Go y Postgres, en vez de node_modules, configuraciones complejas de Docker o Kubernetes, o un exceso de microservicios.
Por qué elegir Go
- Go es una opción para reducir la complejidad excesiva del desarrollo backend, y sus ventajas clave son la compilación rápida, el despliegue como binario único y una gestión estable de dependencias.
- Así como en frontend HTML siguió existiendo como alternativa a la sobrecomplejidad, Go también ha estado ahí por más de 10 años como opción para simplificar el backend.
- Para una app CRUD con formularios simples o unas 40 solicitudes por segundo, es exagerado recurrir a montones de paquetes de Node, herramientas de build de TypeScript, Kubernetes, un equipo de plataforma para Rails o incluso una reescritura en Rust.
- La meta de Go no es la “abstracción ingeniosa”, sino el código fácil de leer, los artefactos listos para desplegar y una carga operativa pequeña.
Un diseño de lenguaje intencionalmente aburrido
- Si Go se siente aburrido, es por diseño: no ofrece abstracciones complejas como decoradores, metaclases, macros, traits o mónadas.
- Sus elementos principales se limitan más o menos a structs, funciones, interfaces, goroutines y channels.
- Apunta a ser lo bastante simple como para leer la especificación en poco tiempo y escribir código productivo ese mismo día.
- Ese aburrimiento funciona como ventaja en el código de un equipo.
- Incluso alguien junior que entró el mes pasado puede leer código escrito hace 2 años por un principal.
- Como
gofmt impone un formato único, se reducen las discusiones sobre estilo.
- El propio lenguaje dificulta meter abstracciones exageradamente complejas en el codebase.
La biblioteca estándar hace el trabajo de un framework
- Go permite crear apps web solo con la biblioteca estándar, sin necesidad de un framework web aparte.
- Con
embed, html/template y net/http, se puede armar una app que incluya plantillas HTML dentro del binario y las renderice con handlers HTTP.
package main
import (
"embed"
"html/template"
"net/http"
)
//go:embed templates/*.html
var files embed.FS
var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", map[string]string{
"Name": "asshole",
})
})
http.ListenAndServe(":8080", nil)
}
- Este ejemplo es una app web funcional, y las plantillas HTML quedan compiladas dentro del binario.
- Sin
webpack, Vite, servidor de desarrollo ni un enorme node_modules, después de go build se puede desplegar un solo archivo.
- Con la biblioteca estándar y las herramientas básicas se pueden cubrir las tareas principales de backend.
- Base de datos:
database/sql
- JSON:
encoding/json
- Llamadas a otros servicios: cliente
net/http
- Ejecución concurrente: palabra clave
go
- Pruebas:
go test
- Benchmarks:
go test -bench
- Profiling:
pprof
Una biblioteca estándar con mucha profundidad
-
io.Reader y io.Writer
io.Reader e io.Writer son interfaces de un solo método, pero funcionan como una base importante en todo el ecosistema de Go.
- Permiten combinar con poco código cosas como conectar el body de una respuesta HTTP a un gzip writer y luego a un archivo en disco.
- Como muchos paquetes importantes comparten estas dos interfaces, el mismo patrón se puede reutilizar una y otra vez.
-
context.Context
context.Context es la forma estándar de propagar cancelación.
- Si el usuario cierra una pestaña del navegador, el request context se cancela, y con eso también pueden cancelarse la consulta a la base de datos y las llamadas HTTP descendentes.
- Para evitar fugas de goroutines o consultas zombis que consumen el pool de conexiones, hay que pasar el context como primer argumento y respetarlo.
-
Paquetes de encoding
encoding/json, encoding/xml, encoding/csv y encoding/binary ya vienen en la biblioteca estándar.
- Como comparten una experiencia de uso similar basada en struct tags y decodificación con punteros, aprender uno facilita usar los demás.
Un modelo de concurrencia que reduce el dolor
- Una goroutine no es un thread del sistema operativo; es una unidad de ejecución con stack administrado que el runtime multiplexa sobre threads del sistema.
- El costo de iniciar una goroutine es de aproximadamente 2 KB, y se pueden crear 100 mil incluso en una laptop.
- Los channels funcionan como tuberías tipadas entre goroutines: una envía, otra recibe, y el runtime se encarga de la sincronización.
- Cuando hace falta estado compartido, se puede usar
sync.Mutex, y el race detector ayuda a encontrar data races.
- Incluso un fetcher HTTP en paralelo puede escribirse sin librerías o frameworks adicionales, y sin rituales de
async/await.
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
resp, _ := http.Get(u)
results <- resp.Status
}(url)
}
for range urls {
fmt.Println(<-results)
}
Ejemplo real de una ruta CRUD
- Incluso una ruta de tipo CRUD que lee posts desde Postgres y renderiza HTML puede mantenerse lo bastante simple como para caber en una sola pantalla.
//go:embed templates/*.html
var tmplFS embed.FS
var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))
type Post struct {
ID int
Title string
Body string
}
func postsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(),
"SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
posts = append(posts, p)
}
tmpl.ExecuteTemplate(w, "posts.html", posts)
}
}
- Este ejemplo muestra en un solo lugar la base de datos, las plantillas y el handler HTTP.
- Como
r.Context() se pasa a la consulta SQL, si la conexión se cierra la consulta también puede cancelarse.
- Se puede leer de arriba hacia abajo y entender cómo funciona, sin ORM, contenedor de DI, capa de servicios ni un directorio
controllers/ lleno de clases base abstractas.
Gestión de dependencias que no te arruina el fin de semana
- Al iniciar un módulo con
go mod init, las dependencias quedan registradas en go.mod y go.sum.
go.sum es, en la práctica, un registro criptográfico de lo que realmente se descargó, lo que permite verificar si entró una dependencia distinta de la esperada.
- No existe la complejidad de un directorio
node_modules, el lockfile drift entre desarrollo y CI, peer dependencies, optional dependencies, devDependencies o peerDependenciesMeta.
- Si se necesita compilar offline,
go mod vendor descarga las dependencias dentro de vendor/, y la toolchain las usa automáticamente.
- También facilita empaquetar todo el proyecto y sus dependencias en un solo tarball, lo que ayuda en operación y revisión de seguridad.
Herramientas que vienen con el compilador
- Las herramientas básicas de Go vienen incluidas, sin plugins de terceros ni archivos de configuración adicionales.
gofmt estandariza el formato del código y reduce discusiones de estilo y diffs innecesarios por espacios.
go vet se usa para detectar errores evidentes.
go test ejecuta las pruebas.
go test -race ejecuta las pruebas con el race detector para encontrar data races.
go test -bench ejecuta benchmarks.
go test -cover permite revisar la cobertura de pruebas.
go tool pprof permite obtener flame graphs de uso de CPU y memoria a través del endpoint HTTP de un servicio en producción en ejecución.
Desplegar se resume en copiar un archivo
- El flujo central de despliegue en Go consiste en compilar el binario, copiarlo al servidor y ejecutarlo.
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
- Este flujo permite desplegar sin Dockerfile, multi-stage build, alertas de CVE en imágenes base, manifests de Kubernetes, charts de Helm, ArgoCD, service mesh ni sidecars.
- Con un binario enlazado estáticamente de unos 12 MB y un archivo unit de systemd de 20 líneas, ya se puede hacer un despliegue de producción.
- Si Docker es indispensable, basta con meter el binario de Go en una imagen
FROM scratch.
Contraste con los frameworks
- Frameworks como Rails, Django, Express o Next.js traen su propia carga: procedimientos de despliegue, ORM, panel admin, middleware, advertencias de npm o cambios en convenciones de routing.
- Un binario de Go se compila y se ejecuta, y tiene como ventaja una estabilidad que puede hacer que siga corriendo incluso dentro de 5 años.
- Frente a la posibilidad de que un framework quede obsoleto más rápido o que sus maintainers terminen quemados, el modelo simple de ejecución de Go resalta todavía más.
Un único binario de Go en vez de microservicios
- Los microservicios no deberían ser la opción por defecto; es mejor escribir primero un monolito.
- La configuración recomendada es un binario de Go, un Postgres y, solo si de verdad hace falta, un Redis.
- Se puede servir HTML y una API JSON en el mismo puerto, ejecutándolo todo en un único VPS.
- Como Go tiene bajo costo por goroutine y un manejo fuerte de concurrencia, puede escalar sin problema hasta 10 mil requests por segundo.
- Si de verdad llega el momento de separar, se puede dividir moviendo paquetes del monolito de Go a repositorios aparte.
- Como las interfaces ya existen, el propio lenguaje empuja naturalmente hacia una estructura que contempla esa separación.
Genéricos y manejo de errores
if err != nil no es un bug, es una feature.
- Obliga a decidir explícitamente qué hacer en cada punto de falla y evita esconder errores.
- Anidar
try/catch no elimina los errores; solo puede ocultarlos hasta que revienten en producción.
- Los genéricos llegaron en Go 1.18, y se pueden usar cuando hagan falta.
Conclusión
- No siempre hacen falta frameworks, microservicios, una reescritura en Rust ni un nuevo metaframework de JavaScript.
- La recomendación es seguir un flujo simple: ejecutar
go mod init, escribir main.go, hacer embed de las plantillas, compilar y desplegar.
- La opción aburrida es la correcta, y Go es esa opción.
1 comentarios
Opiniones de Lobste.rs
No es por culpar al mensajero, pero este tipo de estilo de blog cansa y se siente infantil. Quizá al principio dio risa, pero mientras más se repite, más fastidia de forma exponencial
Aun así, Go está bien. Hace poco me pasé de un proyecto en TypeScript a uno en Go y mi salud mental y la moral en el trabajo están mejorando rápido
Acepto que
if err != nilno sea un bug sino una feature, pero igual me parece el mayor defecto de Go. Si tuviera tipos suma (sum types), podría ser mucho más ergonómico sin depender de aserciones de tipo en tiempo de ejecuciónSi van a escribir así, mínimo que los insultos tengan algo de ingenio
Viendo los demás comentarios, parece una opinión impopular, y no quiero sonar brusco, pero de verdad odio Go
Go es un lenguaje con una sintaxis más o menos aceptable montada sobre un runtime eficiente para concurrencia, y con el peso de Google empujando su ecosistema. Fuera de eso, me parece terrible
El problema principal es que parece diseñado para ignorar deliberadamente décadas de investigación en diseño de lenguajes de programación e incluso prácticas reales de desarrollo. Sí, al final recibió genéricos, pero hasta décadas después
No digo que siempre haya que usar tipos dependientes, pero hay niveles. En Go casi no hay funciones para modelar datos, invariantes ni estructura de código que uno esperaría de un lenguaje moderno. Rust tiene una curva de aprendizaje más dura, pero en esto está muchísimo mejor, y ni siquiera hace falta un sistema de tipos tan sofisticado como el de Rust para que funcione bien. Si te preocupan los tiempos de compilación, se puede construir un sistema de tipos sólido, rápido y expresivo con funciones simples pero útiles
Y
if err != nilme parece la peor forma posible de llenar el código de ruido de manejo de errores. No entiendo por qué el mundo de Go le tiene tanta aversión a los tipos suma. En esto incluso las excepciones de Java son mejores. La realidad es que, como el lenguaje no ofrece mejores herramientas para tratar errores, la gente terminó confundiendo el peor parche posible con una featureSi el texto original no hubiera sido tan presumido, ni habría escrito este comentario. “Simplemente usa X” es una tontería. Usa la herramienta que encaje con tu caso, te resulte cómoda y te haga productivo. Si esa es Go, usa Go; si no, elige otra cosa
En particular, en organizaciones como Google, donde hay miles de desarrolladores y la permanencia en un equipo o empresa específica puede ser corta, eso ayuda
En ese contexto, sobre todo para desarrolladores menos maduros, la ausencia de un sistema de tipos avanzado sí puede ser una ventaja en cierta medida. Porque casi no tienes que pensar en tipos más allá de conceptos muy básicos como tipos primitivos o structs. No te da muchas herramientas para modelar datos, pero a cambio puedes escribir mucho código sin pensarlo demasiado
No me parece muy bueno para la corrección a nivel de lenguaje. Pero en organizaciones grandes se termina dependiendo mucho más de infraestructura alrededor, como análisis de monorepo, CI/CD, pruebas canary y herramientas de observabilidad. Esa infraestructura carga mucho más peso que en organizaciones pequeñas
A mí Go me gusta un poco por esa misma baja carga cognitiva. Porque solo escribo código de vez en cuando en ciertos proyectos y no estoy involucrado a diario en proyectos largos de forma profunda. Poder entrar a una codebase que no he visto en un mes y hacer algo en menos de una hora es una gran ventaja. Aunque si fuera desarrollador de tiempo completo en un proyecto complejo, creo que me gustaría menos
Creo que los desarrolladores de Go se enfocaron en hacer bien lo básico. Porque los lenguajes anteriores y la comunidad de teoría de lenguajes de programación han descuidado lo básico. La gente se obsesiona con el sistema de tipos más completo posible, pero mientras más complejo y expresivo se vuelve, menos rendimiento da esa inversión, y por mucho que optimices el sistema de tipos, eso no compensa un manejo de paquetes horrible, herramientas de build que obligan al equipo a aprender un DSL nuevo, sistemas de documentación que no generan automáticamente información de tipos ni enlaces a documentación de paquetes de terceros, una biblioteca estándar pobre, problemas serios de rendimiento, falta de estrategias de compilación estática, tiempos de build dolorosos, una curva de aprendizaje empinada, un sistema de tipos punitivo, sintaxis difícil de leer o mala integración con el editor
Decir que Go no tiene ninguna capacidad para modelar datos es sencillamente falso. En cualquier lenguaje se pueden modelar datos e invariantes, y Go sí ofrece bastantes herramientas de tipos para hacer cumplir ese modelo
Rust es excelente, y es una gran opción cuando la velocidad de iteración no importa tanto, cuando vas a desplegar sobre bare metal o cuando las exigencias de corrección y rendimiento son muy altas. Pero no me parece un buen valor por defecto para desarrollo de aplicaciones generales, sobre todo en equipo. Sí, escribes mucho
if err != nil, pero no creo que haya nadie cuyo cuello de botella sean las pulsaciones por segundoEs falso decir que
if err != nilno es un bug sino una feature y que te obliga a ver todos los puntos donde algo puede salir malEn la práctica no te obliga. Si no revisas manualmente, es más fácil ignorar un error
En cómo se manejan o propagan errores, Rust sigue siendo el ejemplo que más brilla
Por suerte, todos los proyectos de Go en los que he trabajado en los últimos años usaban golangci-lint encima de las débiles comprobaciones estáticas integradas de Go. Sinceramente, debería ser obligatorio en cualquier proyecto Go
Detesto esta moda de escribir así, pero sí coincido con la idea que quiere transmitir
Es cierto eso de que “no hay un
node_modulesdel tamaño de un Volkswagen”, pero en realidad solo es una caché global de paquetes en~/goen vez de unnode_moduleslocal al proyectowc -l go.sumApenas abrí la página, vi “Hey, dipshit.” y la cerré de inmediato
Tiene el mismo problema que la mayoría de los textos que ensalzan lenguajes de programación. Se enfocan menos en lo grandioso que es el lenguaje actual y más en lo horrible que era el lenguaje anterior
El autor parece haber sufrido muchísimo con Ruby y TypeScript, quizá también con Python, y Go vino a resolverle eso. Pero como yo no uso Ruby ni TypeScript, el texto no me dijo mucho
Siento que he leído docenas de variaciones de esto a lo largo de los años. Usa Haskell porque, a diferencia de Python y JavaScript, tiene tipos estáticos. Usa Rust porque, a diferencia de Perl y Erlang, puedes desplegarlo como un solo binario. Usa Elixir porque, a diferencia de Ruby y Tcl, tiene concurrencia real y canales
Me alegra que el autor haya encontrado un lenguaje que le funciona, pero no voy a seguir ese consejo
El valor cero de Go siempre me ha parecido un defecto. Preferiría que el usuario tuviera que indicar explícitamente los valores por defecto. Fuera de eso, y considerando que no es OCaml, es un lenguaje bastante bueno
boolausente debería tomar el valortrueLa experiencia de despliegue y compilación es excelente, pero odio escribir el lenguaje en sí. Cada vez que lo uso, la experiencia es mala. ¿Hay otros lenguajes con una experiencia de despliegue tan buena sin ser tan restrictivos como Go?
¿Se me está escapando algo de Go?
Hace poco intenté desplegar una aplicación pequeña de Rails y necesitó tanta configuración que definitivamente me hizo apreciar las ventajas de Go
x86_64-unknown-linux-musl. Con eso obtienes un binario estático que simplemente corre en cualquier máquina Linux de 64 bits. Luego lo paso conscpy lo ejecutoAún queda el problema de asignar el puerto y arrancarlo manualmente, pero pienso resolverlo con un poco de magia de systemd
Con el bundler puedes generar un solo ejecutable, soltarlo en una máquina Linux de otra distro, e incluso si no tiene Qt instalado, el usuario puede correr ese ejecutable y toda la GUI funciona
Eso sí, hay una advertencia con los drivers de OpenGL. Sigue siendo posible, pero ya es más complejo que “copiar y ejecutar”
El mayor problema es que Go afirma estar diseñado para concurrencia, pero aun así trae punteros crudos integrados que se pueden compartir por error con facilidad
Que sea aburrido en sí está bien, pero creo que Go fracasa de forma muy particular incluso en ser un lenguaje aburrido de verdad
Dicen “no tiene decoradores”, pero sí tiene struct tags y reflexión. Es difícil entender cómo interactúan esas cosas hasta que las ejecutas
Las interfaces estructurales y la reflexión son una fuente aterradora de comportamiento que cambia a distancia. Basta agregar un método equivocado a un struct para que el comportamiento de una biblioteca cambie por completo
Incluso desde la perspectiva de documentación se siente raro. ¿Por qué no querrías dejar claro que un tipo está pensado para satisfacer cierta interfaz?
No entiendo por qué a las goroutines no simplemente les llaman threads
¿Y por qué los channels tienen que ser una feature del lenguaje? Creo que es porque tardaron 10 años en aceptar que los genéricos sirven para algo más que unos tres tipos
Creo que los channels son parte del runtime para que el scheduler de goroutines los conozca y así pueda despertar más fácilmente a la goroutine receptora cuando un channel deja de estar vacío. Probablemente fue más fácil hacerlo así