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
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.

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/migratepara 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
JoinsouPreloadseletivo). - 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,INSERTmulti-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