Go-ORMs für PostgreSQL im Vergleich: GORM vs. Ent vs. Bun vs. sqlc

Ein praxisorientierter, codeintensiver Blick auf ORMs in Go

Inhaltsverzeichnis

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.

golang + postgresql

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 Joins oder selektives Preload).
  • 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

ORM-Pakete und -Bibliotheken in Go

Abonnieren

Neue Beiträge zu Systemen, Infrastruktur und KI-Engineering.