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

Uma visão prática e com ênfase em código sobre ORMs em Go

Conteúdo da página

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

golang + postgresql

TL;DR

  • GORM: repleto de recursos e conveniente; o mais fácil para “entregar rápido”, mas tem mais sobrecarga em tempo de execução.
  • Ent: esquema-como-código com APIs geradas e tipo-seguras; excelente para grandes bases de código e refatorações.
  • Bun: ORM/construtor de consultas SQL-first leve; rápido com ótimos recursos do Postgres, explícito por design.
  • sqlc (não é um ORM propriamente dito, mas ainda assim): escreva SQL, obtenha Go tipo-seguro; 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/vazão, sobrecarga evitável, operações em lote.
  • Experiência do Desenvolvedor (DX): curva de aprendizado, segurança de tipos, capacidade de depuração, atrito na geração de código.
  • Ecossistema: documentação, exemplos, atividade, integrações (migrações, rastreamento).
  • Conjunto de recursos: relacionamentos, carregamento ansioso (eager loading), migrações, ganchos (hooks), saídas de emergência 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 (em tempo de execução) Sim (tags, Preload/Joins) Auto-migrate (opção opt-in) db.Raw(...) Entrega rápida, recursos ricos, aplicativos CRUD convencionais
Ent Esquema → geração de código → API fluente Alta (em tempo de compilação) Em primeira classe (edges/bordas) SQL Gerado (etapa separada) entsql, SQL personalizado Grandes bases de código, equipes com muitas refatorações, tipagem estrita
Bun Construtor de consultas/ORM SQL-first Média–Alta Explícito (Relation) Pacote de migração separado Natural (construtor + bruto) Serviços conscientes de desempenho, recursos do Postgres
sqlc SQL → funções de geração de código (não é um ORM) Alta (em tempo de compilação) Via junções SQL Ferramenta externa (ex., golang-migrate) É o SQL Controle e velocidade máximos; equipes amigáveis a DBAs

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 exemplo de código go, 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; tenha cuidado em produção)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }

CRUD

func gormCRUD(ctx context.Context, db *gorm.DB) error {
  // Criar
  u := User{Name: "Alice", Email: "alice@example.com"}
  if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }

  // Ler
  var got User
  if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }

  // Atualizar
  if err := db.WithContext(ctx).Model(&got).
    Update("email", "alice+1@example.com").Error; err != nil { return err }

  // Deletar
  if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }

  return nil
}

Notas

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

Ent

Definição de 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 {
  // Criar
  u, err := client.User.Create().
    SetName("Alice").
    SetEmail("alice@example.com").
    Save(ctx)
  if err != nil { return err }

  // Ler
  got, err := client.User.Get(ctx, u.ID)
  if err != nil { return err }

  // Atualizar
  if _, err := client.User.UpdateOneID(got.ID).
    SetEmail("alice+1@example.com").
    Save(ctx); err != nil { return err }

  // Deletar
  if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }

  return nil
}

Notas

  • Tipagem forte ponta a ponta; bordas 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 {
  // Criar
  u := &User{Name: "Alice", Email: "alice@example.com"}
  if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }

  // Ler
  var got User
  if err := db.NewSelect().Model(&got).
    Where("id = ?", u.ID).
    Scan(ctx); err != nil { return err }

  // Atualizar
  if _, err := db.NewUpdate().Model(&got).
    Set("email = ?", "alice+1@example.com").
    WherePK().
    Exec(ctx); err != nil { return err }

  // Deletar
  if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }

  return nil
}

Notas

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

sqlc

O sqlc tecnicamente não é um ORM. Você escreve SQL; ele gera métodos Go tipo-seguros.

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)

  // Criar
  u, err := q.CreateUser(ctx, db.CreateUserParams{
    Name: "Alice", Email: "alice@example.com",
  })
  if err != nil { return err }

  // Ler
  got, err := q.GetUser(ctx, u.ID)
  if err != nil { return err }

  // Atualizar
  up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
    ID: got.ID, Email: "alice+1@example.com",
  })
  if err != nil { return err }

  // Deletar
  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 múltiplas variantes SQL ou combine com um pequeno construtor.

Notas de Desempenho

  • GORM: conveniente, mas adiciona sobrecarga de reflexão/abstração. Bom 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: fina camada 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 lotes (COPY, INSERT multi-linha) para alta vazão.
  • Perfilize o SQL: EXPLAIN ANALYZE, índices, índices cobertos, evite viagens de ida e volta 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 acentuada para padrões avançados.
  • Ent: ótima documentação; o passo de geração de código é 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 detalhes 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.

Destaques de Recursos

  • Relacionamentos e carregamento ansioso: todos lidam com relacionamentos; GORM (tags + Preload/Joins), Ent (bordas + .With...()), Bun (Relation(...)), sqlc (você escreve as junções).
  • Migrações: GORM (auto-migrate; cuidado em produção), Ent (SQL gerado/diferença), Bun (bun/migrate), sqlc (ferramentas externas).
  • Ganchos/Extensibilidade: GORM (callbacks/plugins), Ent (ganchos/middleware + template/codegen), Bun (ganchos de consulta estilo middleware, SQL bruto fácil), sqlc (componha na camada de aplicativos).
  • JSON/Arrays (Postgres): Bun e GORM têm helpers legais; Ent/sqlc lidam via tipos personalizados ou SQL.

Quando Escolher o Que

  • Escolha GORM se você quer máxima conveniência, recursos ricos e prototipagem 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 moldadas em SQL com confortos de ORM onde ajudam.
  • Escolha sqlc se você (e sua equipe) prefere SQL puro com bindings Go tipo-seguros e zero sobrecarga em tempo de execução. O sqlc também é uma opção natural para o lado do modelo de leitura de uma arquitetura CQRS em Go, onde as consultas são moldadas para os chamadores em vez de entidades de domínio e o SQL explícito dá controle total sobre a projeção.

Se você ainda está equilibrando esta escolha de ORM em relação ao estilo de integração e limites de serviço, este visão geral da arquitetura de aplicativos ajuda a colocar a decisão em um contexto de produção mais amplo.


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

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.