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

Uma abordagem prática e focada em código sobre ORMs em GO

Conteúdo da página

Os ORMs mais proeminentes para GO são GORM, Ent, Bun e sqlc. Aqui está uma breve 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ó entregar”, mas tem mais sobrecarga em tempo de execução.
  • Ent: esquema como código com APIs geradas e seguras quanto ao tipo; excelente para bases de código grandes e refactors.
  • Bun: leve, construtor de consultas/ORM com SQL em primeiro plano; 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 ao tipo; o melhor desempenho e controle crus, 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.
  • DX (Experiência do Desenvolvedor): curva de aprendizado, segurança de tipos, depurabilidade, atrito no codegen.
  • Ecossistema: documentação, exemplos, atividade, integrações (migrações, tracing).
  • Conjunto de recursos: relacionamentos, carregamento antecipado (eager loading), migrações, hooks, válvulas de escape para SQL cru.
Ferramenta Paradigma Segurança de Tipos Relacionamentos Migrações Ergonomia de SQL Cru Caso de Uso Típico
GORM ORM estilo Active Record Média (tempo de execução) Sim (tags, Preload/Joins) Auto-migrate (opt-in) db.Raw(...) Entrega rápida, recursos ricos, apps CRUD convencionais
Ent Esquema → codegen → API fluente Alta (tempo de compilação) De primeira classe (borda/edges) SQL gerado (etapa separada) entsql, SQL personalizado Bases de código grandes, equipes com muitos refactors, tipagem estrita
Bun Construtor de consultas/ORM com SQL em primeiro plano Média–Alta Explícito (Relation) Pacote separado de migração Natural (builder + cru) Serviços conscientes em 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; equipes amigáveis com 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'

Importações (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 {
  // 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.
  • Helper 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; bordas (edges) para relacionamentos.
  • Migrações geradas ou use sua ferramenta de migração de preferência.

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ícito 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 seguros quanto ao tipo.

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 múltiplas variantes de SQL ou combine com um pequeno builder.

Notas de Desempenho

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

Dicas gerais

  • Use pgx para o driver (v5) e context em todo lugar.
  • Prefira agrupamento em lote (COPY, INSERT multi-linha) para alta vazão.
  • Faça perfil do SQL: EXPLAIN ANALYZE, índices, índices de cobertura, 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 íngreme para padrões avançados.
  • Ent: ótima documentação; a etapa de codegen é a principal mudança de modelo mental; super amigável a refactors.
  • Bun: consultas legíveis e previsíveis; comunidade menor, mas ativa; excelentes conveniências do Postgres.
  • sqlc: dependências de tempo de execução mínimas; integra-se bem com ferramentas de migração e CI; excelente para equipes confortáveis com SQL.

Destaques de Recursos

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

Uma observação transversal para todos os quatro: o Go 1.27 agora suporta encoding/json com o motor v2, que preserva o comportamento, mas altera alguns textos de erro e, sob a API v2 explícita, a saída de slices nil e chaves de mapa. Se seus modelos fazem round-trip de JSON, verifique o que o guia de upgrade do Go 1.27 diz sobre fixtures de teste de ouro antes de fazer o upgrade.

Quando Escolher Cada Um

  • Escolha GORM se você quiser 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 mantibilidade de longo prazo em equipes maiores.
  • Escolha Bun se você quiser desempenho e consultas em formato SQL explícito com os confortos de um ORM onde ajudar.
  • Escolha sqlc se você (e sua equipe) preferir SQL puro com bindings Go seguros quanto ao tipo e zero sobrecarga em tempo de execução. O sqlc também é uma ótima escolha 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á a você controle total sobre a projeção.

Se você ainda está equilibrando essa escolha de ORM contra o estilo de integração e os limites de serviço, este resumo de arquitetura de aplicativo ajuda a situar 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.