Go-ORMs für PostgreSQL im Vergleich: GORM vs. Ent vs. Bun vs. sqlc
Ein praxisorientierter, codeintensiver Blick auf ORMs in Go
Die bekanntesten 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 zum „direkt Veröffentlichen", hat aber mehr Laufzeit-Overhead.
- Ent: Schema-as-Code mit generierten, typsicheren APIs; hervorragend für große Codebasen und Refactors.
- Bun: Leichtgewichtig, SQL-first Query Builder/ORM; schnell mit hervorragenden Postgres-Funktionen, explizit durch Design.
- sqlc (kein ORM im eigentlichen Sinne, aber dennoch): SQL schreiben, typsicheres Go erhalten; beste rohe Leistung und Kontrolle, keine Laufzeit-Magie.
Auswahlkriterien und kurzer Vergleich
Meine Kriterien sind:
- Leistung: Latenz/Durchsatz, vermeidbarer Overhead, Batch-Operationen.
- DX (Entwicklertexte): Lernkurve, Typsicherheit, Debuggbarkeit, Codegen-Reibungsverluste.
- Ökosystem: Dokumentation, Beispiele, Aktivität, Integrationen (Migrationen, Tracing).
- Funktionsumfang: Beziehungen, Eager Loading, Migrationen, Hooks, Ausweichmöglichkeiten für rohes SQL.
| Tool | Paradigma | Typsicherheit | Beziehungen | Migrationen | Ergonomie für rohes SQL | Typischer Anwendungsfall |
|---|---|---|---|---|---|---|
| GORM | Active Record-ähnliches ORM | Mittel (Laufzeit) | Ja (Tags, Preload/Joins) | Auto-Migrate (opt-in) | db.Raw(...) |
Schnelle Lieferung, reiche Funktionen, konventionelle CRUD-Apps |
| Ent | Schema → Codegen → fluide API | Hoch (Compile-Zeit) | Erste Klasse (edges) | Generiertes SQL (separater Schritt) | entsql, eigenes SQL |
Große Codebasen, refactor-intensive Teams, strikte Typisierung |
| Bun | SQL-first Query Builder/ORM | Mittel–Hoch | Explizit (Relation) |
Separates Migrate-Paket | Natürlich (Builder + raw) | Leistungsbewusste Services, Postgres-Funktionen |
| sqlc | SQL → Codegen-Funktionen (kein ORM) | Hoch (Compile-Zeit) | Über SQL-Joins | Externes Tool (z. B. golang-migrate) | Es ist SQL | Maximale Kontrolle & Geschwindigkeit; DBA-freundliche Teams |
CRUD im Beispiel
Einrichtung (PostgreSQL)
Verwenden Sie pgx oder den nativen PG-Treiber des Tools. Beispiel-DSN:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
Imports (gemeinsam für alle ORMs)
Am Anfang jeder Datei mit Go-Code-Beispielen hinzufügen:
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
Init
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 Produktion)
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
}
Notizen
- Beziehungen über Struct-Tags +
Preload/Joins. - Transaktions-Helfer:
db.Transaction(func(tx *gorm.DB) error { ... }).
Ent
Schema-Definition (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
Init
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
}
Notizen
- Starre Typisierung von Anfang bis Ende; Kanten (edges) für Beziehungen.
- Generierte Migrationen oder Verwendung Ihres bevorzugten Migrationstools.
Bun
Init
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
}
Notizen
- Explizite Joins/Eager Loading mit
.Relation("..."). - Separates
bun/migrate-Paket 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" # or "github.com/jackc/pgx/v5"
Queries (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
Verwendung
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
}
Notizen
- Bringen Sie Ihre eigenen Migrationen mit (z. B.
golang-migrate). - Für dynamische Queries: Schreiben Sie mehrere SQL-Varianten oder kombinieren Sie sie mit einem kleinen Builder.
Leistungsnotizen
- GORM: Komfortabel, fügt aber Reflection/Abstraktions-Overhead hinzu. Gut für typisches CRUD; achten Sie auf N+1-Queries (bevorzugen Sie
Joinsoder selektivesPreload). - Ent: Generierter Code vermeidet Reflection; gut für komplexe Schemata. Oft schneller als ORMs mit schwerer Laufzeit-Magie.
- Bun: Dünn über
database/sql; schnell, explizit, hervorragend für Batch-Operationen und große Ergebnismengen. - sqlc: Im Grunde rohe SQL-Leistung mit Compile-Zeit-Sicherheit.
Allgemeine Tipps
- Verwenden Sie pgx als Treiber (v5) und context überall.
- Bevorzugen Sie Batching (
COPY, Mehrzeilen-INSERT) für hohen Durchsatz. - Profilieren Sie SQL:
EXPLAIN ANALYZE, Indizes, Covering-Indizes, vermeiden Sie unnötige Roundtrips. - Wiederverwenden Sie Verbindungen; stimmen Sie die Pool-Größe basierend auf der Arbeitslast ab.
Entwicklertexte und Ökosystem
- GORM: Größte Community, viele Beispiele/Plugins; steilere Lernkurve für fortgeschrittene Muster.
- Ent: Tolle Dokumentation; der Codegen-Schritt ist der Haupt-Mental-Shift; super refactor-freundlich.
- Bun: Lesbare, vorhersehbare Queries; kleinere, aber aktive Community; hervorragende Postgres-Extras.
- sqlc: Minimale Laufzeit-Abhängigkeiten; integriert sich nahtlos mit Migrationstools und CI; hervorragend für Teams, die mit SQL vertraut sind.
Funktions-Highlights
- Beziehungen & Eager Loading: Alle handhaben Beziehungen; GORM (Tags +
Preload/Joins), Ent (Edges +.With...()), Bun (Relation(...)), sqlc (Sie schreiben die Joins). - Migrationen: GORM (Auto-Migrate; Vorsicht in 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 (Zusammensetzen in Ihrer App-Schicht).
- JSON/Arrays (Postgres): Bun und GORM haben nette Helfer; Ent/sqlc handhaben sie über benutzerdefinierte Typen oder SQL.
Ein querschnittsbezogener Hinweis für alle vier: Go 1.27 unterstützt nun encoding/json mit der v2-Engine, die das Verhalten beibehält, aber einige Fehlermeldungen ändert und, unter der expliziten v2-API, die Ausgabe für nil-Slices und Map-Keys. Wenn Ihre Modelle JSON-Roundtrips durchführen, prüfen Sie, was der Go 1.27-Upgrade-Guide zu Golden-Test-Fixtures vor dem Upgrade sagt.
Wann man was auswählt
- Wählen Sie GORM, wenn Sie maximalen Komfort, reiche Funktionen und schnelles Prototyping für konventionelle CRUD-Services wünschen.
- Wählen Sie Ent, wenn Sie Compile-Zeit-Sicherheit, explizite Schemata und langfristige Wartbarkeit in größeren Teams schätzen.
- Wählen Sie Bun, wenn Sie Leistung und explizite SQL-artige Queries mit ORM-Komfort dort wünschen, wo er hilft.
- Wählen Sie sqlc, wenn Sie (und Ihr Team) pures SQL mit typsicheren Go-Bindings und null Laufzeit-Overhead bevorzugen. sqlc ist auch ein natürlicher Fit für die Read-Model-Seite einer CQRS-Architektur in Go, wo Queries für Aufrufer und nicht für Domänenentitäten geformt sind und explizites SQL Ihnen volle Kontrolle über die Projektion gibt.
Wenn Sie diese ORM-Auswahl immer noch im Hinblick auf Integrationsstil und Service-Grenzen abwägen, hilft diese App-Architektur-Übersicht dabei, die Entscheidung in einem breiteren Produktionskontext zu platzieren.
Minimale 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