Comparación de ORMs de Go para PostgreSQL: GORM frente a Ent, Bun y sqlc
Una visión práctica y centrada en el código sobre las ORMs en Go
Los ORM más destacados para GO son GORM, Ent, Bun y sqlc. Aquí tienes una pequeña comparación de ellos con ejemplos de operaciones CRUD en GO puro.

TL;DR
- GORM: repleto de funciones y conveniente; el más fácil para “solo lanzar el producto”, pero tiene más sobrecarga en tiempo de ejecución.
- Ent: esquema como código con APIs generadas y seguras en cuanto a tipos; excelente para bases de código grandes y refactorizaciones.
- Bun: ligero, creador de consultas/ORM centrado en SQL; rápido con excelentes funciones de Postgres, explícito por diseño.
- sqlc (no es estrictamente un ORM, pero aun así): escribes SQL y obtienes Go seguro en cuanto a tipos; mejor rendimiento y control crudos, sin magia en tiempo de ejecución.
Criterios de selección y comparación rápida
Mis criterios son:
- Rendimiento: latencia/throughput, sobrecarga evitable, operaciones por lotes (batch).
- Experiencia de desarrollo (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 funciones: relaciones, carga anticipada (eager loading), migraciones, hooks, escapes a SQL crudo.
| Herramienta | Paradigma | Seguridad de tipos | Relaciones | Migraciones | Ergonomía de SQL crudo | Caso de uso típico |
|---|---|---|---|---|---|---|
| GORM | ORM de estilo Active Record | Media (tiempo de ejecución) | Sí (etiquetas, Preload/Joins) | Migración automática (opt-in) | db.Raw(...) |
Entrega rápida, funciones ricas, aplicaciones CRUD convencionales |
| Ent | Esquema → generación de código → API fluida | Alta (compilación) | De primera clase (aristas) | SQL generado (paso separado) | entsql, SQL personalizado |
Bases de código grandes, equipos con muchas refactorizaciones, tipado estricto |
| Bun | Creador de consultas/ORM centrado en SQL | Media–Alta | Explícitas (Relation) |
Paquete de migración separado | Natural (builder + crudo) | Servicios con conciencia del rendimiento, funciones de Postgres |
| sqlc | SQL → funciones generadas de código (no es un ORM) | Alta (compilación) | A través de joins SQL | Herramienta externa (p. ej., golang-migrate) | Es SQL | Máximo control y velocidad; equipos amigables con DBA |
CRUD con ejemplos
Configuración (PostgreSQL)
Usa pgx o el conductor PG nativo de la herramienta. Ejemplo de DSN:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
Importaciones (comunes para todos los ORMs)
Al principio de cada archivo con código go agregas:
import (
"context"
"os"
)
Modelaremos una simple tabla users:
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. - Asistente 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 principio a fin; aristas 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
- Joins/carga anticipada explícita con
.Relation("..."). - Paquete separado
bun/migratepara migraciones.
sqlc
sqlc técnicamente no es un ORM. Escribes SQL; genera métodos de Go seguros en cuanto a 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 (p. ej.,
golang-migrate). - Para consultas dinámicas: escribe múltiples variantes de SQL o combínalas con un pequeño creador de consultas.
Notas de rendimiento
- GORM: conveniente pero agrega sobrecarga de reflexión/abstracción. Bien para CRUD típico; 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 los ORMs pesados con magia en tiempo de ejecución.
- Bun: delgado sobre
database/sql; rápido, explícito, excelente para operaciones por lotes y conjuntos de resultados grandes. - sqlc: esencialmente rendimiento de SQL crudo con seguridad en tiempo de compilación.
Consejos generales
- Usa pgx para el conductor (v5) y context en todas partes.
- Prefiere el procesamiento por lotes (
COPY,INSERTde varias filas) para alto rendimiento. - Perfil SQL:
EXPLAIN ANALYZE, índices, índices de cobertura, evita recorridos innecesarios. - Reutiliza conexiones; ajusta el tamaño de la piscina según la carga de trabajo.
Experiencia del desarrollador y Ecosistema
- GORM: comunidad más grande, muchos ejemplos/plugins; curva de aprendizaje más empinada para patrones avanzados.
- Ent: excelente documentación; el paso de generación de código es el cambio principal en el modelo mental; súper amigable con refactorizaciones.
- Bun: consultas legibles y predecibles; comunidad más pequeña pero activa; excelentes características 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 funciones
- Relaciones y carga anticipada: todos manejan relaciones; GORM (etiquetas +
Preload/Joins), Ent (aristas +.With...()), Bun (Relation(...)), sqlc (tú escribes los joins). - Migraciones: GORM (migración automática; cuidado en producción), Ent (SQL generado/diferenciado), Bun (
bun/migrate), sqlc (herramientas externas). - Hooks/Extensibilidad: GORM (callbacks/plugins), Ent (hooks/middleware + plantilla/generación de código), Bun (hooks de consulta tipo middleware, SQL crudo fácil), sqlc (componer en tu capa de aplicación).
- JSON/Arrays (Postgres): Bun y GORM tienen buenos asistentes; Ent/sqlc los manejan a través de tipos personalizados o SQL.
Una nota transversal para los cuatro: Go 1.27 ahora respalda encoding/json con el motor v2, lo que preserva el comportamiento pero cambia algunos textos de error y, bajo la API v2 explícita, la salida de slices nil y claves de mapa. Si tus modelos hacen ida y vuelta de JSON, revisa lo que dice la guía de actualización de Go 1.27 sobre los fixtures de pruebas doradas antes de actualizar.
Cuándo elegir qué
- Elige GORM si quieres conveniencia máxima, funciones 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 con forma de SQL explícita con las comodidades de un ORM donde ayude.
- Elige sqlc si tú (y tu equipo) prefieren SQL puro con bindings de Go seguros en cuanto a 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 diseñadas para los llamadores en lugar de las 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, esta visión general de la arquitectura de la aplicación ayuda a situar 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