Confronto degli ORM Go per PostgreSQL: GORM vs Ent vs Bun vs sqlc

Una panoramica pratica, ricca di codice, sugli ORM in Go

Indice

Gli ORM per Go più diffusi sono GORM, Ent, Bun e sqlc. Ecco un breve confronto tra loro con esempi di operazioni CRUD in Go puro.

golang + postgresql

TL;DR

  • GORM: ricco di funzionalità e comodo; il più facile da usare per “lanciare subito” il prodotto, ma ha un maggiore overhead a runtime.
  • Ent: schema come codice con API generate e type-safe; eccellente per codebase di grandi dimensioni e refactoring.
  • Bun: query builder/ORM leggero, orientato al SQL; veloce con eccellenti funzionalità per Postgres, esplicito per design.
  • sqlc (non è propriamente un ORM, ma comunque): scrivi SQL, ottieni Go type-safe; le migliori prestazioni e controllo raw, nessuna magia a runtime.

Criteri di selezione e confronto rapido

I miei criteri sono:

  • Prestazioni: latenza/throughput, overhead evitabile, operazioni batch.
  • DX (Developer Experience): curva di apprendimento, type safety, debuggabilità, attrito nella generazione del codice.
  • Ecosistema: documentazione, esempi, attività, integrazioni (migrazioni, tracing).
  • Set di funzionalità: relazioni, caricamento eager (eager loading), migrazioni, hook, escape hatch per SQL raw.
Strumento Paradigma Type Safety 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 (compile-time) Prima classe (edges) SQL generato (passo separato) entsql, SQL personalizzato Codebase di grandi dimensioni, team con refactoring intensivo, typing rigoroso
Bun Query builder/ORM SQL-first Media-Alta Esplicito (Relation) Pacchetto migrate separato Naturale (builder + raw) Servizi attenti alle prestazioni, funzionalità Postgres
sqlc SQL → funzioni codegen (non un ORM) Alta (compile-time) Tramite join SQL Strumento esterno (es. golang-migrate) È SQL Massimo controllo e velocità; team amichevoli verso i DBA

CRUD per Esempio

Configurazione (PostgreSQL)

Usa pgx o il driver PG nativo dello strumento. Esempio DSN:

export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'

Importazioni (comuni per 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; fai 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 struct tags + 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(),
  }
}

Genera il 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

  • Type safety forte end-to-end; edges per le relazioni.
  • Migrazioni generate o usa il tuo strumento 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/caricamento eager esplicito con .Relation("...").
  • Pacchetto bun/migrate separato per 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" # o "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;

Genera

sqlc generate

Utilizzo

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

  • Porta le tue migrazioni (es. golang-migrate).
  • Per query dinamiche: scrivi più varianti SQL o combinale 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 Joins o Preload selettivo).
  • Ent: il codice generato evita la riflessione; buono per schemi complessi. Spesso più veloce degli ORM pesanti basati su magia a runtime.
  • Bun: sottile sopra database/sql; veloce, esplicito, ottimo per operazioni batch e grandi set di risultati.
  • sqlc: essenzialmente prestazioni SQL raw con sicurezza a compile-time.

Consigli generali

  • Usa pgx come driver (v5) e context ovunque.
  • Preferisci il batching (COPY, INSERT multi-riga) per un alto throughput.
  • Profila SQL: EXPLAIN ANALYZE, indici, indici covering, evita roundtrip non necessari.
  • Riutilizza le connessioni; regola la dimensione del pool in base al carico di lavoro.

Esperienza dello sviluppatore ed Ecosistema

  • GORM: comunità più grande, molti esempi/plugin; curva di apprendimento più ripida per pattern avanzati.
  • Ent: ottima documentazione; il passo di codegen è il principale cambiamento di mentalità; super amichevole per il refactoring.
  • Bun: query leggibili e prevedibili; comunità più piccola ma attiva; eccellenti funzionalità per Postgres.
  • sqlc: dipendenze runtime minime; si integra bene con strumenti di migrazione e CI; eccellente per team confortevoli con SQL.

Punti Salienti delle Funzionalità

  • Relazioni & caricamento eager: tutti gestiscono le relazioni; GORM (tag + Preload/Joins), Ent (edges + .With...()), Bun (Relation(...)), sqlc (tu scrivi i join).
  • Migrazioni: GORM (auto-migrate; attenzione in produzione), Ent (SQL generato/diff), Bun (bun/migrate), sqlc (strumenti esterni).
  • Hook/Estensibilità: GORM (callbacks/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 utili; Ent/sqlc gestiscono tramite tipi personalizzati o SQL.

Quando Scegliere Cosa

  • Scegli GORM se vuoi la massima comodità, funzionalità ricche e prototipazione rapida per servizi CRUD convenzionali.
  • Scegli Ent se valuti la sicurezza a compile-time, schemi espliciti e mantenibilità a lungo termine in team più grandi.
  • Scegli Bun se vuoi prestazioni e query SQL-shaped espliciti con i comfort dell’ORM dove aiutano.
  • Scegli sqlc se tu (e il tuo team) preferisci SQL puro con binding Go type-safe e zero overhead a runtime. sqlc è anche l’ideale per il lato read model di un’architettura CQRS in Go, dove le query sono modellate per i chiamanti piuttosto che per le entità di dominio e SQL esplicito ti dà il pieno controllo sulla proiezione.

Se stai ancora bilanciando questa scelta ORM rispetto allo stile di integrazione e ai confini dei servizi, questa panoramica sull’architettura dell’app aiuta a inquadrare la decisione in un contesto produttivo 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

Pacchetti e librerie ORM in GO

Iscriviti

Ricevi nuovi articoli su sistemi, infrastruttura e ingegneria AI.