Vergleich von Go-ORMs für PostgreSQL: GORM vs. Ent vs. Bun vs. sqlc
Ein praktischer, codeintensiver Blick auf ORMs in Go
Die prominentesten ORMs für Go sind GORM, Ent, Bun und sqlc. Hier ist ein kleiner Vergleich von ihnen mit Beispielen für CRUD-Operationen in reinem Go.

TL;DR
- GORM: funktionsreich und komfortabel; am einfachsten „loslegen“, aber mit höherem Laufzeit-Overhead.
- Ent: Schema-as-Code mit generierten, typsicheren APIs; ausgezeichnet für große Codebasen und Refactorings.
- Bun: schlanker, SQL-first Query Builder/ORM; schnell mit großartigen Postgres-Funktionen, explizit durch Design.
- sqlc (kein ORM im eigentlichen Sinne, aber dennoch): Schreiben Sie SQL, erhalten Sie typsicheres Go; beste rohe Leistung und Kontrolle, kein Laufzeit-Magic.
Auswahlkriterien und schneller Vergleich
Meine Kriterien sind:
- Leistung: Latenz/Durchsatz, vermeidbarer Overhead, Batch-Operationen.
- DX (Developer Experience): Lernkurve, Typsicherheit, Debuggbarkeit, Reibungsverluste bei der Codegenerierung.
- Ökosystem: Dokumentation, Beispiele, Aktivität, Integrationen (Migrationen, Tracing).
- Funktionsumfang: Beziehungen, Eager Loading, Migrationen, Hooks, Ausweichmöglichkeiten für rohes SQL.
| Werkzeug | Paradigma | Typsicherheit | Beziehungen | Migrationen | Ergonomie rohen SQLs | Typischer Anwendungsfall |
|---|---|---|---|---|---|---|
| GORM | Active-Record-Style ORM | Mittel (Laufzeit) | Ja (Tags, Preload/Joins) | Auto-migrate (opt-in) | db.Raw(...) |
Schnelle Entwicklung, reiche Features, konventionelle CRUD-Apps |
| Ent | Schema → Codegen → fluent API | Hoch (Compile-time) | Erster Klasse (Edges) | Generiertes SQL (separater Schritt) | entsql, benutzerdefiniertes SQL |
Große Codebasen, refactoringstarke Teams, strikte Typisierung |
| Bun | SQL-first Query Builder/ORM | Mittel–Hoch | Explizit (Relation) |
Separates Migrate-Package | Natürlich (Builder + raw) | Leistungsbewusste Services, Postgres-Features |
| sqlc | SQL → Codegen-Funktionen (kein ORM) | Hoch (Compile-time) | Über SQL Joins | Externes Tool (z. B. golang-migrate) | Es ist SQL | Maximale Kontrolle & Geschwindigkeit; DBA-freundliche Teams |
CRUD am Beispiel
Einrichtung (PostgreSQL)
Verwenden Sie pgx oder den nativen PG-Treiber des Werkzeugs. Beispiel-DNS:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
Imports (gemeinsam für alle ORMs)
Fügen Sie am Anfang jeder Datei mit Go-Code-Beispielen Folgendes hinzu:
import (
"context"
"os"
)
Wir modellieren eine einfache users-Tabelle:
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
GORM
Initialisierung
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 (optional; Vorsicht in der Produktion)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }
CRUD
func gormCRUD(ctx context.Context, db *gorm.DB) error {
// Erstellen
u := User{Name: "Alice", Email: "alice@example.com"}
if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }
// Lesen
var got User
if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }
// Aktualisieren
if err := db.WithContext(ctx).Model(&got).
Update("email", "alice+1@example.com").Error; err != nil { return err }
// Löschen
if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }
return nil
}
Hinweise
- Beziehungen über Struct-Tags +
Preload/Joins. - Transaktions-Helfer:
db.Transaction(func(tx *gorm.DB) error { ... }).
Ent
Schemadefinition (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(),
}
}
Code generieren
go run entgo.io/ent/cmd/ent generate ./ent/schema
Initialisierung
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 {
// Erstellen
u, err := client.User.Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
if err != nil { return err }
// Lesen
got, err := client.User.Get(ctx, u.ID)
if err != nil { return err }
// Aktualisieren
if _, err := client.User.UpdateOneID(got.ID).
SetEmail("alice+1@example.com").
Save(ctx); err != nil { return err }
// Löschen
if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }
return nil
}
Hinweise
- Strikte Typisierung durchgängig; Edges für Beziehungen.
- Generierte Migrationen oder verwenden Sie Ihr bevorzugtes Migrationstool.
Bun
Initialisierung
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 {
// Erstellen
u := &User{Name: "Alice", Email: "alice@example.com"}
if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }
// Lesen
var got User
if err := db.NewSelect().Model(&got).
Where("id = ?", u.ID).
Scan(ctx); err != nil { return err }
// Aktualisieren
if _, err := db.NewUpdate().Model(&got).
Set("email = ?", "alice+1@example.com").
WherePK().
Exec(ctx); err != nil { return err }
// Löschen
if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }
return nil
}
Hinweise
- Explizite Joins/Eager Loading mit
.Relation("..."). - Separates
bun/migrate-Package für Migrationen.
sqlc
sqlc ist technisch gesehen kein ORM. Sie schreiben SQL; es generiert typsichere Go-Methoden.
sqlc.yaml
version: "2"
sql:
- engine: postgresql
queries: db/queries
schema: db/migrations
gen:
go:
package: db
out: internal/db
sql_package: "database/sql" # oder "github.com/jackc/pgx/v5"
Abfragen (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;
Generieren
sqlc generate
Nutzung
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)
// Erstellen
u, err := q.CreateUser(ctx, db.CreateUserParams{
Name: "Alice", Email: "alice@example.com",
})
if err != nil { return err }
// Lesen
got, err := q.GetUser(ctx, u.ID)
if err != nil { return err }
// Aktualisieren
up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
ID: got.ID, Email: "alice+1@example.com",
})
if err != nil { return err }
// Löschen
if err := q.DeleteUser(ctx, up.ID); err != nil { return err }
return nil
}
Hinweise
- Bringen Sie Ihre eigenen Migrationen mit (z. B.
golang-migrate). - Für dynamische Abfragen: Schreiben Sie mehrere SQL-Varianten oder kombinieren Sie sie mit einem kleinen Builder.
Leistungsaspekte
- GORM: Komfortabel, fügt aber Reflection-/Abstraktions-Overhead hinzu. Gut für typisches CRUD; achten Sie auf N+1-Abfragen (bevorzugen Sie
Joinsoder selektivesPreload). - Ent: Generierter Code vermeidet Reflection; gut für komplexe Schemata. Oft schneller als ORMs mit viel Laufzeit-Magic.
- Bun: Dünne Schicht über
database/sql; schnell, explizit, großartig für Batch-Operationen und große Ergebnismengen. - sqlc: Im Wesentlichen die Leistung von rohem SQL mit Compile-Time-Sicherheit.
Allgemeine Tipps
- Verwenden Sie pgx als Treiber (v5) und context überall.
- Bevorzugen Sie Batches (
COPY, multi-rowINSERT) für hohen Durchsatz. - Profilen Sie SQL:
EXPLAIN ANALYZE, Indizes, Covering-Indizes, vermeiden Sie unnötige Roundtrips. - Reuse connections; passen Sie die Pool-Größe an die Arbeitslast an.
Developer Experience und Ökosystem
- GORM: Größte Community, viele Beispiele/Plugins; steilere Lernkurve für fortgeschrittene Muster.
- Ent: Großartige Dokumentation; der Codegen-Schritt ist der Hauptwechsel im Denkmodell; super refactor-freundlich.
- Bun: Lesbare, vorhersagbare Abfragen; kleinere, aber aktive Community; ausgezeichnete Postgres-Features.
- sqlc: Minimale Laufzeit-Abhängigkeiten; integriert sich gut mit Migrationstools und CI; hervorragend für Teams, die mit SQL vertraut sind.
Funktionshervorhebungen
- Beziehungen & Eager Loading: Alle behandeln Beziehungen; GORM (Tags +
Preload/Joins), Ent (Edges +.With...()), Bun (Relation(...)), sqlc (Sie schreiben die Joins). - Migrationen: GORM (Auto-migrate; Vorsicht in der Produktion), Ent (generiertes/diff SQL), Bun (
bun/migrate), sqlc (externe Tools). - Hooks/Erweiterbarkeit: GORM (Callbacks/Plugins), Ent (Hooks/Middleware + Template/Codegen), Bun (Middleware-ähnliche Query-Hooks, einfaches rohes SQL), sqlc (Zusammenstellung in Ihrer App-Schicht).
- JSON/Arrays (Postgres): Bun und GORM haben nette Helfer; Ent/sqlc behandeln dies über benutzerdefinierte Typen oder SQL.
Was wann wählen
- Wählen Sie GORM, wenn Sie maximalen Komfort, reiche Features und schnelles Prototyping für konventionelle CRUD-Services wünschen.
- Wählen Sie Ent, wenn Sie Compile-Time-Sicherheit, explizite Schemata und langfristige Wartbarkeit in größeren Teams schätzen.
- Wählen Sie Bun, wenn Sie Leistung und explizite SQL-geformte Abfragen mit ORM-Bequemlichkeiten dort, wo es hilft, möchten.
- Wählen Sie sqlc, wenn Sie (und Ihr Team) reines SQL mit typsicheren Go-Bindings und Null-Laufzeit-Overhead bevorzugen. sqlc ist auch eine natürliche Passform für die Lese-Modell-Seite einer CQRS-Architektur in Go, wo Abfragen für Aufrufer geformt werden, anstatt für Domain-Entitäten, und explizites SQL Ihnen volle Kontrolle über die Projektion gibt.
Wenn Sie diese ORM-Entwahl noch gegen Integrationsstil und Service-Grenzen abwägen, hilft dieser Überblick zur App-Architektur, um die Entscheidung in einen breiteren Produktionskontext einzuordnen.
Minimales docker-compose.yml für lokales PostgreSQL
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