Confronto tra gli ORM Go per PostgreSQL: GORM vs Ent vs Bun vs sqlc
Un approfondimento pratico e ricco di codice sugli ORM in GO
Gli ORM più diffusi per GO sono GORM, Ent, Bun e sqlc. Ecco un breve confronto tra di essi con esempi di operazioni CRUD in puro GO.

TL;DR
- GORM: ricco di funzionalità e comodo; il più semplice per “spedire subito” il codice, ma ha un overhead di runtime maggiore.
- Ent: schema-as-code con API generate e type-safe; eccellente per codebase ampie e refactor.
- Bun: leggero, query builder/ORM SQL-first; veloce con ottime funzionalità Postgres, esplicito per design.
- sqlc (non è un ORM in senso stretto, ma): scrivi SQL, ottieni Go type-safe; migliori prestazioni raw e controllo, nessuna magia al runtime.
Criteri di Selezione e Confronto Rapido
I miei criteri sono:
- Prestazioni: latenza/throughput, overhead evitabile, operazioni batch.
- DX: curva di apprendimento, sicurezza dei tipi, debuggabilità, attrito nella codegen.
- Ecosistema: documentazione, esempi, attività, integrazioni (migrations, tracing).
- Set di funzionalità: relazioni, eager loading, migrazioni, hook, vie di fuga per SQL raw.
| Strumento | Paradigma | Sicurezza dei Tipi | Relazioni | Migrazioni | Ergonomia SQL Raw | Caso d’Uso Tipico |
|---|---|---|---|---|---|---|
| GORM | ORM stile Active Record | Media (runtime) | Sì (tag, Preload/Joins) | Auto-migrate (opt-in) | db.Raw(...) |
Consegna rapida, funzionalità ricche, app CRUD convenzionali |
| Ent | Schema → codegen → API fluent | Alta (compilation-time) | Prima classe (edges) | SQL generato (passo separato) | entsql, SQL custom |
Codebase ampi, team con molti refactor, tipizzazione rigorosa |
| Bun | Query builder/ORM SQL-first | Media–Alta | Esplicito (Relation) |
Pacco di migrazione separato | Naturale (builder + raw) | Servizi attenti alle prestazioni, funzionalità Postgres |
| sqlc | SQL → funzioni codegen (non un ORM) | Alta (compilation-time) | Tramite join SQL | Strumento esterno (es. golang-migrate) | È il SQL | Massimo controllo e velocità; team amichevoli ai DBA |
CRUD con Esempi
Configurazione (PostgreSQL)
Usa pgx o il driver PG nativo dello strumento. Esempio DSN:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
Import (comuni a tutti gli ORM)
All’inizio di ogni file con codice go aggiungi:
import (
"context"
"os"
)
Modelleremo una semplice tabella users:
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
GORM
Inizializzazione
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-migrate (opzionale; attenzione in produzione)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }
CRUD
func gormCRUD(ctx context.Context, db *gorm.DB) error {
// Create
u := User{Name: "Alice", Email: "alice@example.com"}
if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }
// Read
var got User
if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }
// Update
if err := db.WithContext(ctx).Model(&got).
Update("email", "alice+1@example.com").Error; err != nil { return err }
// Delete
if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }
return nil
}
Note
- Relazioni tramite tag delle struct +
Preload/Joins. - Helper per transazioni:
db.Transaction(func(tx *gorm.DB) error { ... }).
Ent
Definizione dello schema (in 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(),
}
}
Generazione del codice
go run entgo.io/ent/cmd/ent generate ./ent/schema
Inizializzazione
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 {
// Create
u, err := client.User.Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
if err != nil { return err }
// Read
got, err := client.User.Get(ctx, u.ID)
if err != nil { return err }
// Update
if _, err := client.User.UpdateOneID(got.ID).
SetEmail("alice+1@example.com").
Save(ctx); err != nil { return err }
// Delete
if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }
return nil
}
Note
- Tipizzazione forte end-to-end; edges per le relazioni.
- Migrazioni generate o usa il tuo tool di migrazione preferito.
Bun
Inizializzazione
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 {
// Create
u := &User{Name: "Alice", Email: "alice@example.com"}
if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }
// Read
var got User
if err := db.NewSelect().Model(&got).
Where("id = ?", u.ID).
Scan(ctx); err != nil { return err }
// Update
if _, err := db.NewUpdate().Model(&got).
Set("email = ?", "alice+1@example.com").
WherePK().
Exec(ctx); err != nil { return err }
// Delete
if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }
return nil
}
Note
- Join/eager loading espliciti con
.Relation("..."). - Pacchetto separato
bun/migrateper le migrazioni.
sqlc
sqlc tecnicamente non è un ORM. Scrivi SQL; genera metodi Go type-safe.
sqlc.yaml
version: "2"
sql:
- engine: postgresql
queries: db/queries
schema: db/migrations
gen:
go:
package: db
out: internal/db
sql_package: "database/sql" # or "github.com/jackc/pgx/v5"
Query (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;
Generazione
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)
// Create
u, err := q.CreateUser(ctx, db.CreateUserParams{
Name: "Alice", Email: "alice@example.com",
})
if err != nil { return err }
// Read
got, err := q.GetUser(ctx, u.ID)
if err != nil { return err }
// Update
up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
ID: got.ID, Email: "alice+1@example.com",
})
if err != nil { return err }
// Delete
if err := q.DeleteUser(ctx, up.ID); err != nil { return err }
return nil
}
Note
- Portare le proprie migrazioni (es.
golang-migrate). - Per query dinamiche: scrivere più varianti SQL o combinare con un piccolo builder.
Note sulle Prestazioni
- GORM: comodo ma aggiunge overhead di riflessione/astrazione. Va bene per CRUD tipici; fai attenzione alle query N+1 (preferisci
JoinsoPreloadselettivo). - Ent: il codice generato evita la riflessione; buono per schemi complessi. Spesso più veloce di ORM pesanti con magia runtime.
- Bun: sottile su
database/sql; veloce, esplicito, ottimo per operazioni batch e set di risultati grandi. - sqlc: essenzialmente prestazioni SQL raw con sicurezza in fase di compilazione.
Consigli generali
- Usa pgx per il driver (v5) e context ovunque.
- Preferisci il batching (
COPY,INSERTmulti-riga) per alto throughput. - Profila il SQL:
EXPLAIN ANALYZE, indici, indici coprenti, evita roundtrips non necessari. - Riusa le connessioni; regola la dimensione del pool in base al carico di lavoro.
Esperienza dello Sviluppatore ed Ecosistema
- GORM: community più grande, tanti esempi/plugin; curva di apprendimento più ripida per pattern avanzati.
- Ent: ottima documentazione; il passo della codegen è la principale svolta mentale; molto amichevole per i refactor.
- Bun: query leggibili e prevedibili; community più piccola ma attiva; ottime funzionalità Postgres.
- sqlc: dipendenze runtime minime; si integra bene con tool di migrazione e CI; eccellente per team a loro agio con SQL.
Funzionalità in Evidenza
- Relazioni ed eager loading: tutti gestiscono le relazioni; GORM (tag +
Preload/Joins), Ent (edges +.With...()), Bun (Relation(...)), sqlc (scrivi tu i join). - Migrazioni: GORM (auto-migrate; attenzione in prod), Ent (SQL generato/diff), Bun (
bun/migrate), sqlc (strumenti esterni). - Hook/Estendibilità: GORM (callback/plugin), Ent (hook/middleware + template/codegen), Bun (hook di query simili a middleware, SQL raw facile), sqlc (componi nel tuo layer applicativo).
- JSON/Array (Postgres): Bun e GORM hanno helper comodi; Ent/sqlc gestiscono tramite tipi custom o SQL.
Una nota trasversale per tutti e quattro: Go 1.27 ora supporta encoding/json con il motore v2, che preserva il comportamento ma cambia alcuni testi di errore e, sotto l’API v2 esplicita, l’output per slice nil e map-key. Se i tuoi modelli fanno round-trip JSON, controlla cosa dice la guida all’upgrade Go 1.27 sui fixture dei test d’oro prima di aggiornare.
Quando Scegliere Cosa
- Scegli GORM se vuoi massima comodità, funzionalità ricche e prototipazione rapida per servizi CRUD convenzionali.
- Scegli Ent se valorizzi la sicurezza in fase di compilazione, schemi espliciti e manutenibilità a lungo termine in team più grandi.
- Scegli Bun se vuoi prestazioni e query esplicite con forma SQL con i comfort degli ORM dove aiutano.
- Scegli sqlc se (e il tuo team) preferite il puro SQL con binding Go type-safe e zero overhead runtime. sqlc è anche una scelta naturale per il lato read model di un’architettura CQRS in Go dove le query sono formate per i chiamanti piuttosto che per le entità di dominio e il SQL esplicito ti dà pieno controllo sulla proiezione.
Se stai ancora bilanciando la scelta di questo ORM rispetto allo stile di integrazione e ai confini dei servizi, questa panoramica sull’architettura dell’applicazione aiuta a posizionare la decisione in un contesto di produzione più ampio.
docker-compose.yml Minimo per PostgreSQL Locale
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