Comparación de ORMs de Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc
Una mirada práctica, con abundante código, a los ORM en Go
Las ORMs más destacadas para Go son GORM, Ent, Bun y sqlc. Aquí hay una pequeña comparación de ellas con ejemplos de operaciones CRUD en Go puro.

Resumen (TL;DR)
- GORM: Llena de funciones y conveniente; es la más fácil para “entregar rápido”, pero tiene más sobrecarga en tiempo de ejecución.
- Ent: Esquema como código con APIs generadas y seguras en tipos; excelente para bases de código grandes y refactorizaciones.
- Bun: Constructor de consultas/ORM ligero y orientado a SQL; rápido con grandes características de Postgres, explícito por diseño.
- sqlc (no es una ORM propiamente dicha, pero aún así): escribe SQL, obtén Go seguro en tipos; el mejor rendimiento y control en bruto, sin magia en tiempo de ejecución.
Criterios de Selección y Comparación Rápida
Mis criterios son:
- Rendimiento: latencia/ancho de banda, sobrecarga evitable, operaciones por lotes.
- Experiencia del Desarrollador (DX): curva de aprendizaje, seguridad de tipos, depurabilidad, fricción en la generación de código.
- Ecosistema: documentación, ejemplos, actividad, integraciones (migraciones, trazabilidad).
- Conjunto de características: relaciones, carga ansiosa (eager loading), migraciones, ganchos (hooks), escapes para SQL en bruto.
| Herramienta | Paradigma | Seguridad de Tipos | Relaciones | Migraciones | Ergonomía de SQL en Bruto | Caso de Uso Típico |
|---|---|---|---|---|---|---|
| GORM | ORM estilo Active Record | Media (en tiempo de ejecución) | Sí (etiquetas, Preload/Joins) | Auto-migración (opcional) | db.Raw(...) |
Entrega rápida, características ricas, aplicaciones CRUD convencionales |
| Ent | Esquema → generación de código → API fluida | Alta (en tiempo de compilación) | De primera clase (bordes/aristas) | SQL generado (paso separado) | entsql, SQL personalizado |
Bases de código grandes, equipos con muchas refactorizaciones, tipado estricto |
| Bun | Constructor de consultas/ORM orientado a SQL | Media-Alta | Explícito (Relation) |
Paquete de migración separado | Natural (constructor + en bruto) | Servicios conscientes del rendimiento, características de Postgres |
| sqlc | SQL → funciones generadas (no es una ORM) | Alta (en tiempo de compilación) | Mediante uniones SQL | Herramienta externa (ej., golang-migrate) | Es SQL | Máximo control y velocidad; equipos amigables con DBAs |
CRUD por Ejemplo
Configuración (PostgreSQL)
Usa pgx o el conductor nativo de PG de la herramienta. Ejemplo de DSN:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
Importaciones (comunes para todas las ORMs)
Al principio de cada archivo con código go ejemplo, añade:
import (
"context"
"os"
)
Modelaremos una tabla users simple:
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
GORM
Inicialización
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type User struct {
ID int64 `gorm:"primaryKey"`
Name string
Email string `gorm:"uniqueIndex"`
}
func newGorm() (*gorm.DB, error) {
dsn := os.Getenv("DATABASE_URL")
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}
// Auto-migración (opcional; ten cuidado en producción)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }
CRUD
func gormCRUD(ctx context.Context, db *gorm.DB) error {
// Crear
u := User{Name: "Alice", Email: "alice@example.com"}
if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }
// Leer
var got User
if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }
// Actualizar
if err := db.WithContext(ctx).Model(&got).
Update("email", "alice+1@example.com").Error; err != nil { return err }
// Eliminar
if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }
return nil
}
Notas
- Relaciones mediante etiquetas de estructura +
Preload/Joins. - Helper de transacciones:
db.Transaction(func(tx *gorm.DB) error { ... }).
Ent
Definición del esquema (en ent/schema/user.go):
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int64("id").Unique().Immutable(),
field.String("name"),
field.String("email").Unique(),
}
}
Generar código
go run entgo.io/ent/cmd/ent generate ./ent/schema
Inicialización
import (
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"your/module/ent"
)
func newEnt() (*ent.Client, error) {
dsn := os.Getenv("DATABASE_URL")
drv, err := sql.Open(dialect.Postgres, dsn)
if err != nil { return nil, err }
return ent.NewClient(ent.Driver(drv)), nil
}
CRUD
func entCRUD(ctx context.Context, client *ent.Client) error {
// Crear
u, err := client.User.Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
if err != nil { return err }
// Leer
got, err := client.User.Get(ctx, u.ID)
if err != nil { return err }
// Actualizar
if _, err := client.User.UpdateOneID(got.ID).
SetEmail("alice+1@example.com").
Save(ctx); err != nil { return err }
// Eliminar
if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }
return nil
}
Notas
- Tipado fuerte de extremo a extremo; bordes (edges) para relaciones.
- Migraciones generadas o usa tu herramienta de migración preferida.
Bun
Inicialización
import (
"database/sql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
_ "github.com/jackc/pgx/v5/stdlib"
)
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Email string `bun:",unique,notnull"`
}
func newBun() (*bun.DB, error) {
dsn := os.Getenv("DATABASE_URL")
sqldb, err := sql.Open("pgx", dsn)
if err != nil { return nil, err }
return bun.NewDB(sqldb, pgdialect.New()), nil
}
CRUD
func bunCRUD(ctx context.Context, db *bun.DB) error {
// Crear
u := &User{Name: "Alice", Email: "alice@example.com"}
if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }
// Leer
var got User
if err := db.NewSelect().Model(&got).
Where("id = ?", u.ID).
Scan(ctx); err != nil { return err }
// Actualizar
if _, err := db.NewUpdate().Model(&got).
Set("email = ?", "alice+1@example.com").
WherePK().
Exec(ctx); err != nil { return err }
// Eliminar
if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }
return nil
}
Notas
- Uniones explícitas/carga ansiosa con
.Relation("..."). - Paquete separado
bun/migratepara migraciones.
sqlc
sqlc técnicamente no es una ORM. Escribes SQL; genera métodos Go seguros en tipos.
sqlc.yaml
version: "2"
sql:
- engine: postgresql
queries: db/queries
schema: db/migrations
gen:
go:
package: db
out: internal/db
sql_package: "database/sql" # o "github.com/jackc/pgx/v5"
Consultas (db/queries/users.sql)
-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email;
-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;
-- name: UpdateUserEmail :one
UPDATE users SET email = $2 WHERE id = $1
RETURNING id, name, email;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
Generar
sqlc generate
Uso
import (
"database/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"your/module/internal/db"
)
func sqlcCRUD(ctx context.Context) error {
dsn := os.Getenv("DATABASE_URL")
sqldb, err := sql.Open("pgx", dsn)
if err != nil { return err }
q := db.New(sqldb)
// Crear
u, err := q.CreateUser(ctx, db.CreateUserParams{
Name: "Alice", Email: "alice@example.com",
})
if err != nil { return err }
// Leer
got, err := q.GetUser(ctx, u.ID)
if err != nil { return err }
// Actualizar
up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
ID: got.ID, Email: "alice+1@example.com",
})
if err != nil { return err }
// Eliminar
if err := q.DeleteUser(ctx, up.ID); err != nil { return err }
return nil
}
Notas
- Aporta tus propias migraciones (ej.,
golang-migrate). - Para consultas dinámicas: escribe múltiples variantes SQL o combínalas con un constructor pequeño.
Notas sobre el Rendimiento
- GORM: conveniente pero añade sobrecarga de reflexión/abstracción. Aceptable para CRUD típico; ten cuidado con las consultas N+1 (prefiere
JoinsoPreloadselectivo). - Ent: el código generado evita la reflexión; bueno para esquemas complejos. A menudo más rápido que ORMs pesados con magia en tiempo de ejecución.
- Bun: capa delgada sobre
database/sql; rápido, explícito, excelente para operaciones por lotes y conjuntos de resultados grandes. - sqlc: esencialmente rendimiento de SQL en bruto con seguridad en tiempo de compilación.
Consejos generales
- Usa pgx para el conductor (v5) y context en todas partes.
- Prefiere operaciones por lotes (
COPY,INSERTmulti-fila) para alto ancho de banda. - Perfil SQL:
EXPLAIN ANALYZE, índices, índices de cobertura, evita viajes de ida y vuelta innecesarios. - Reutiliza conexiones; ajusta el tamaño del pool según la carga de trabajo.
Experiencia del Desarrollador y Ecosistema
- GORM: comunidad más grande, muchos ejemplos/plugin; curva de aprendizaje más pronunciada para patrones avanzados.
- Ent: excelente documentación; el paso de generación de código es el principal cambio de modelo mental; súper amigable para refactorizaciones.
- Bun: consultas legibles y predecibles; comunidad más pequeña pero activa; excelentes detalles de Postgres.
- sqlc: dependencias mínimas en tiempo de ejecución; se integra bien con herramientas de migración y CI; excelente para equipos cómodos con SQL.
Puntos Destacados de las Características
- Relaciones y carga ansiosa: todos manejan relaciones; GORM (etiquetas +
Preload/Joins), Ent (bordes +.With...()), Bun (Relation(...)), sqlc (tú escribes las uniones). - Migraciones: GORM (auto-migración; cuidado en producción), Ent (SQL generado/diferencias), Bun (
bun/migrate), sqlc (herramientas externas). - Ganchos/Extensibilidad: GORM (callbacks/plugins), Ent (ganchos/middleware + plantillas/generación de código), Bun (ganchos de consulta estilo middleware, SQL en bruto fácil), sqlc (compón en tu capa de aplicación).
- JSON/Arrays (Postgres): Bun y GORM tienen helpers agradables; Ent/sqlc lo manejan mediante tipos personalizados o SQL.
Cuándo Elegir Qué
- Elige GORM si quieres máxima conveniencia, características ricas y prototipado rápido para servicios CRUD convencionales.
- Elige Ent si valoras la seguridad en tiempo de compilación, esquemas explícitos y mantenibilidad a largo plazo en equipos más grandes.
- Elige Bun si quieres rendimiento y consultas en forma de SQL explícitas con comodidades de ORM donde ayudan.
- Elige sqlc si tú (y tu equipo) prefieren SQL puro con bindings Go seguros en tipos y cero sobrecarga en tiempo de ejecución. sqlc también es una opción natural para el lado del modelo de lectura de una arquitectura CQRS en Go, donde las consultas están moldeadas para los llamadores en lugar de entidades de dominio y el SQL explícito te da control total sobre la proyección.
Si aún estás equilibrando esta elección de ORM contra el estilo de integración y los límites del servicio, este resumen de arquitectura de aplicaciones ayuda a colocar la decisión en un contexto de producción más amplio.
docker-compose.yml Mínimo para PostgreSQL Local
version: "3.8"
services:
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
ports: ["5432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 5s
timeout: 3s
retries: 5
Paquetes y librerías ORM en GO
Otros Enlaces Útiles
- Hoja de Trucos de Golang
- Rendimiento de AWS Lambda: JavaScript vs Python vs Golang
- Corrigiendo el error de AutoMigrate de GORM en PostgreSQL
- Reranking de documentos de texto con Ollama y el modelo de Embedding Qwen3 - en Go
- Reranking de documentos de texto con Ollama y el modelo de Reranker Qwen3 - en Go