Comparando ORMs em Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc

Uma visão prática e focada em código sobre ORMs em GO.

Conteúdo da página

Os ORMs mais populares para GO são GORM, Ent, Bun e sqlc. Aqui está uma pequena comparação entre eles com exemplos de operações CRUD em GO puro.

golang + postgresql

TL;DR

  • GORM: repleto de recursos e conveniente; o mais fácil para “lançar rapidamente”, mas tem mais sobrecarga em tempo de execução.
  • Ent: esquema como código com APIs geradas e tipadas; excelente para grandes bases de código e refatorações.
  • Bun: construtor de consultas/ORM leve e focado em SQL; rápido com ótimos recursos do Postgres, explícito por design.
  • sqlc (não é exatamente um ORM, mas ainda assim): escreva SQL, obtenha Go tipado; melhor desempenho bruto e controle, sem mágica em tempo de execução.

Critérios de Seleção e Comparação Rápida

Meus critérios são:

  • Desempenho: latência/throughput, sobrecarga evitável, operações em lote.
  • Experiência do Desenvolvedor (DX): curva de aprendizado, segurança de tipos, depurabilidade, atrito com codegen.
  • Ecossistema: documentação, exemplos, atividade, integrações (migrações, tracing).
  • Conjunto de recursos: relacionamentos, carregamento antecipado (eager loading), migrações, hooks, fugas para SQL bruto.
Ferramenta Paradigma Segurança de Tipos Relacionamentos Migrações Ergonomia de SQL Bruto Caso de Uso Típico
GORM ORM estilo Active Record Média (tempo de execução) Sim (tags, Preload/Joins) Auto-migrar (opt-in) db.Raw(...) Entrega rápida, recursos ricos, aplicativos CRUD convencionais
Ent Esquema → codegen → API fluente Alta (tempo de compilação) De primeira classe (edges) SQL Gerado (passo separado) entsql, SQL personalizado Grandes bases de código, equipes com muitas refatorações, tipagem estrita
Bun Construtor de consultas/ORM focado em SQL Média–Alta Explícito (Relation) Pacote de migração separado Natural (builder + bruto) Serviços conscientes de desempenho, recursos do Postgres
sqlc SQL → funções codegen (não é um ORM) Alta (tempo de compilação) Via joins SQL Ferramenta externa (ex: golang-migrate) É SQL puro Controle máximo e velocidade; equipes amigáveis a DBA

CRUD por Exemplo

Configuração (PostgreSQL)

Use pgx ou o driver PG nativo da ferramenta. Exemplo de DSN:

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

Imports (comuns para todos os ORMs)

No início de cada arquivo com código go exemplo, adicione:

import (
  "context"
  "os"
)

Modelaremos uma tabela simples users:

CREATE TABLE IF NOT EXISTS users (
  id    BIGSERIAL PRIMARY KEY,
  name  TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE
);

GORM

Inicialização

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 (opcional; cuidado em produção)
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
}

Notas

  • Relacionamentos via tags de struct + Preload/Joins.
  • Auxiliar de transação: db.Transaction(func(tx *gorm.DB) error { ... }).

Ent

Definição do esquema (em 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(),
  }
}

Gerar código

go run entgo.io/ent/cmd/ent generate ./ent/schema

Inicialização

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
}

Notas

  • Tipagem forte de ponta a ponta; edges para relacionamentos.
  • Migrações geradas ou use sua ferramenta de migração de escolha.

Bun

Inicialização

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
}

Notas

  • Joins/carregamento antecipado explícitos com .Relation("...").
  • Pacote separado bun/migrate para migrações.

sqlc

sqlc tecnicamente não é um ORM. Você escreve SQL; ele gera métodos Go tipados.

sqlc.yaml

version: "2"
sql:
  - engine: postgresql
    queries: db/queries
    schema: db/migrations
    gen:
      go:
        package: db
        out: internal/db
        sql_package: "database/sql" # ou "github.com/jackc/pgx/v5"

Consultas (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;

Gerar

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
}

Notas

  • Traga suas próprias migrações (ex: golang-migrate).
  • Para consultas dinâmicas: escreva várias variantes de SQL ou combine com um pequeno construtor.

Notas de Desempenho

  • GORM: conveniente, mas adiciona sobrecarga de reflexão/abstração. Ótimo para CRUD típico; cuidado com consultas N+1 (prefira Joins ou Preload seletivo).
  • Ent: código gerado evita reflexão; bom para esquemas complexos. Frequentemente mais rápido que ORMs pesados com mágica em tempo de execução.
  • Bun: camada fina sobre database/sql; rápido, explícito, ótimo para operações em lote e grandes conjuntos de resultados.
  • sqlc: essencialmente desempenho de SQL bruto com segurança em tempo de compilação.

Dicas gerais

  • Use pgx para o driver (v5) e context em todos os lugares.
  • Prefira agrupamento (COPY, INSERT multi-linha) para alto throughput.
  • Perfume SQL: EXPLAIN ANALYZE, índices, índices de cobertura, evite viagens desnecessárias.
  • Reutilize conexões; ajuste o tamanho do pool com base na carga de trabalho.

Experiência do Desenvolvedor e Ecossistema

  • GORM: maior comunidade, muitos exemplos/plugins; curva de aprendizado mais íngreme para padrões avançados.
  • Ent: ótima documentação; o passo de codegen é a principal mudança de modelo mental; super amigável para refatoração.
  • Bun: consultas legíveis e previsíveis; comunidade menor, mas ativa; excelentes recursos do Postgres.
  • sqlc: dependências mínimas em tempo de execução; integra-se bem com ferramentas de migração e CI; excelente para equipes confortáveis com SQL.

Destaque de Recursos

  • Relacionamentos e carregamento antecipado: todos lidam com relacionamentos; GORM (tags + Preload/Joins), Ent (edges + .With...()), Bun (Relation(...)), sqlc (você escreve os joins).
  • Migrações: GORM (auto-migrate; cuidado em produção), Ent (gerado/diff SQL), Bun (bun/migrate), sqlc (ferramentas externas).
  • Hooks/Extensibilidade: GORM (callbacks/plugins), Ent (hooks/middleware + template/codegen), Bun (hooks de consulta estilo middleware, SQL bruto fácil), sqlc (componha na camada da sua aplicação).
  • JSON/Arrays (Postgres): Bun e GORM têm ótimos helpers; Ent/sqlc lidam via tipos personalizados ou SQL.

Quando Escolher o Que

  • Escolha GORM se você quer conveniência máxima, recursos ricos e prototipação rápida para serviços CRUD convencionais.
  • Escolha Ent se você valoriza segurança em tempo de compilação, esquemas explícitos e manutenibilidade a longo prazo em equipes maiores.
  • Escolha Bun se você quer desempenho e consultas explícitas com formato SQL, com o conforto de um ORM onde ajuda.
  • Escolha sqlc se você (e sua equipe) preferirem SQL puro com vinculações Go tipadas e sobrecarga zero em tempo de execução.

docker-compose.yml Mínimo para PostgreSQL Local

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

Pacotes e libs de ORM em GO