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

Uma análise prática, rica 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 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 “só lançar”, mas tem mais sobrecarga em tempo de execução.
  • Ent: esquema como código com APIs geradas e seguras quanto a tipos; excelente para bases de código grandes e refactors.
  • Bun: leve, construtor de consultas/ORM centrado em SQL; 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 seguro quanto a tipos; 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.
  • UX (Experiência de Desenvolvimento): curva de aprendizado, segurança de tipos, depurabilidade, atrito na geração de código.
  • Ecossistema: documentação, exemplos, atividade, integrações (migrações, tracing).
  • Conjunto de recursos: relacionamentos, carregamento antecipado (eager loading), migrações, hooks, saídas 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-migrate (opcional) db.Raw(...) Entrega rápida, recursos ricos, apps CRUD convencionais
Ent Esquema → geração de código → API fluida Alta (tempo de compilação) Primeira classe (edges) SQL gerado (etapa separada) entsql, SQL personalizado Bases de código grandes, times que refatoram muito, tipagem estrita
Bun Construtor de consultas/ORM centrado em SQL Média–Alta Explícito (Relation) Pacote de migração separado Natural (builder + raw) Serviços conscientes do desempenho, recursos do Postgres
sqlc SQL → funções geradas (não é um ORM) Alta (tempo de compilação) Via joins SQL Ferramenta externa (ex.: golang-migrate) É o próprio SQL Controle e velocidade máximos; times amigáveis ao DBA

CRUD por Exemplo

Configuração (PostgreSQL)

Use o 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 simples tabela users:

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

GORM

Iniciaçã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
}

Observações

  • Relacionamentos via tags de struct + Preload/Joins.
  • Helper para 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

Iniciaçã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
}

Observações

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

Bun

Iniciaçã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
}

Observações

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

sqlc

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

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
}

Observações

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

Notas de Desempenho

  • GORM: conveniente, mas adiciona sobrecarga de reflexão/abstração. Adequado 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: fino 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 o pgx para o driver (v5) e context em todos os lugares.
  • Prefira agrupamento (batching) (COPY, INSERT multi-linha) para alto throughput.
  • Perfilhe SQL: EXPLAIN ANALYZE, índices, índices cobrindo (covering indexes), evite idas e vindas 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; a etapa de geração de código é a principal mudança de modelo mental; muito amigável para refactors.
  • Bun: consultas legíveis e previsíveis; comunidade menor, mas ativa; excelente em 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 times confortáveis com SQL.

Destaques de Recursos

  • Relacionamentos & eager loading: todos tratam 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 (SQL gerado/diferencial), 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 (compose na sua camada de aplicação).
  • JSON/Arrays (Postgres): Bun e GORM têm ótimos helpers; Ent/sqlc tratam via tipos personalizados ou SQL.

Quando Escolher o Que

  • Escolha GORM se você quer conveniência máxima, 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 times maiores.
  • Escolha Bun se você quer desempenho e consultas explícitas formatadas em SQL com confortos de ORM onde ajudem.
  • Escolha sqlc se você (e seu time) preferem SQL puro com bindings Go seguros quanto a tipos e zero sobrecarga em tempo de execução. O sqlc também é um encaixe natural para o lado de modelo de leitura de uma arquitetura CQRS em Go, onde as consultas são formatadas para os chamadores em vez de entidades de domínio e o SQL explícito lhe dá controle total sobre a projeção.

Se você ainda está balanceando esta escolha de ORM contra o estilo de integração e fronteiras de serviço, esta visão geral de arquitetura de aplicativo 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 bibliotecas de ORM em GO

Subscrever

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