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

Uma visão prática e com muita codificação sobre ORMs no GO ```

Conteúdo da página

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

golang + postgresql

TL;DR

  • GORM: rico em recursos e conveniente; mais fácil de “só enviar”, mas tem mais sobrecarga de tempo de execução.
  • Ent: schema como código com APIs geradas e seguras por tipo; excelente para grandes bases de código e refatorações.
  • Bun: leve, construtor de consultas/ORM com foco em SQL; rápido com ótimas funcionalidades do Postgres, explícito por design.
  • sqlc (não é um ORM por si só, mas ainda assim): escreva SQL, obtenha Go seguro por tipo; melhor desempenho bruto e controle, sem magia de 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.
  • DX: curva de aprendizado, segurança de tipo, debugabilidade, fricção de geração de código.
  • Ecosistema: documentação, exemplos, atividade, integrações (migrações, rastreamento).
  • Conjunto de recursos: relações, carregamento antecipado, migrações, gatilhos, saídas de SQL brutas.
Ferramenta Paradigma Segurança de Tipo Relações Migrações Ergonomia de SQL Bruto Caso de Uso Típico
GORM ORM do tipo ActiveRecord Média (em tempo de execução) Sim (tags, Preload/Joins) Auto-migrate (opcional) db.Raw(...) Entrega rápida, recursos ricos, aplicações CRUD convencionais
Ent Schema → geração de código → API fluente Alta (em tempo de compilação) Primeira classe (edges) SQL gerado (passo separado) entsql, SQL personalizado Grandes bases de código, equipes com refatoração intensa, tipagem estrita
Bun Construtor de consultas/ORM com foco em SQL Média–Alta Explícito (Relation) Pacote separado para migração Natural (construtor + bruto) Serviços com foco em desempenho, funcionalidades do Postgres
sqlc SQL → geração de funções (não é um ORM) Alta (em tempo de compilação) Via junções SQL Ferramenta externa (ex: golang-migrate) É SQL Máximo de controle e velocidade; equipes amigáveis com DBA

CRUD por Exemplo

Configuração (PostgreSQL)

Use pgx ou o driver nativo do PG da ferramenta. Exemplo 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 código GO exemplo adicione:

import (
  "context"
  "os"
)

Vamos modelar uma tabela simples users:

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

GORM

Iniciar

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 }

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

  return nil
}

Notas

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

Ent

Definição de schema (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

Iniciar

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 }

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

  return nil
}

Notas

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

Bun

Iniciar

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 }

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

  return nil
}

Notas

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

sqlc

sqlc tecnicamente não é um ORM. Você escreve SQL; ele gera métodos Go seguros por 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"

Queries (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 }

  // Excluir
  if err := q.DeleteUser(ctx, up.ID); err != nil { return err }

  return nil
}

Notas

  • Leve 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 sobre Desempenho

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

Dicas gerais

  • Use pgx como driver (v5) e contexto em todos os lugares.
  • Prefira batches (COPY, INSERT multi-row) para alta throughput.
  • Analise SQL: EXPLAIN ANALYZE, índices, índices cobrindo, evite viagens desnecessárias.
  • Reutilize conexões; ajuste o tamanho da pool com base na carga de trabalho.

Experiência do Desenvolvedor e Ecosistema

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

Destaque de Recursos

  • Relações & carregamento antecipado: todos lidam com relações; GORM (tags + Preload/Joins), Ent (edges + .With...()), Bun (Relation(...)), sqlc (você escreve as junções).
  • Migrações: GORM (auto-migrate; cuidado em produção), Ent (SQL gerado/diff), Bun (bun/migrate), sqlc (ferramentas externas).
  • Gatilhos/Extensibilidade: GORM (callbacks/plugins), Ent (gatilhos/middleware + template/codegen), Bun (gatilhos de consulta semelhantes a middleware, SQL bruto fácil), sqlc (componha na camada de aplicação).
  • JSON/Arrays (Postgres): Bun e GORM têm bons helpers; Ent/sqlc lidam com tipos personalizados ou SQL.

Quando Escolher o Que

  • Escolha GORM se quiser a máxima conveniência, recursos ricos e prototipagem rápida para serviços CRUD convencionais.
  • Escolha Ent se valorizar a segurança em tempo de compilação, esquemas explícitos e manutenibilidade a longo prazo em equipes maiores.
  • Escolha Bun se quiser desempenho e consultas SQL explícitas com confortos de ORM onde ajudam.
  • Escolha sqlc se você (e sua equipe) preferirem SQL puro com vinculações seguras de Go e zero sobrecarga de 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 bibliotecas ORM em GO