Contexto
GORM e sqlx ocupam extremos opostos do espectro de acesso a dados em Go, e escolher entre eles é escolher quanta abstração você quer entre o seu código e o SQL que roda no banco. GORM é um ORM completo: você descreve suas tabelas como structs Go, declara associações com tags, e a biblioteca gera o SQL, cuida das migrações, dispara hooks de ciclo de vida e resolve o carregamento de relações por você. sqlx faz o oposto por design — é uma extensão fina sobre a database/sql da biblioteca padrão. Ela não esconde o SQL; ela só remove o trabalho chato de mapear colunas em campos de struct. Você escreve a query, ela faz o scan do resultado direto no seu tipo.
A tensão central é controle versus conveniência. Com GORM, um db.Create(&user) pode inserir o usuário e suas associações numa cadeia de callbacks que você não vê — ótimo para velocidade, menos óbvio quando precisa depurar performance. Com sqlx, toda query que atinge o banco está escrita no seu código, visível e intencional; o custo é que tudo é responsabilidade sua, das migrações aos JOINs. Nenhum dos dois é "melhor": eles otimizam variáveis diferentes. GORM otimiza a velocidade de escrever CRUD e navegar relações. sqlx otimiza a transparência e o overhead mínimo sobre a stack padrão do Go.
Quando escolher GORM
GORM é a escolha certa quando o volume de CRUD é alto e as relações entre entidades são o coração do domínio. Declarar associações uma vez na struct e depois carregá-las com Preload economiza um trabalho real de SQL repetitivo, e AutoMigrate mantém o schema em sincronia com os modelos sem você escrever DDL à mão. Hooks como BeforeCreate centralizam regras de ciclo de vida (gerar um UUID, normalizar um campo) no modelo, em vez de espalhá-las por cada ponto de inserção.
package main
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Email string `gorm:"uniqueIndex"`
Posts []Post // has many
}
type Post struct {
ID uint `gorm:"primaryKey"`
Title string
UserID uint
}
func main() {
db, err := gorm.Open(postgres.Open("postgres://localhost/app"), &gorm.Config{})
if err != nil {
panic(err)
}
db.AutoMigrate(&User{}, &Post{}) // cria/altera as tabelas a partir das structs
db.Create(&User{Name: "Ada", Email: "ada@example.com"})
var users []User
// Preload evita o N+1: uma query para users, outra para todos os posts
db.Preload("Posts").Where("name = ?", "Ada").Find(&users)
}
O exemplo mostra o ganho e o risco no mesmo lugar. O ganho: associação declarada, migração automática e carregamento das relações em duas queries com um único Preload. O risco: se você iterar users e acessar user.Posts sem o Preload, GORM pode disparar uma query por usuário — o clássico problema N+1, que fica escondido justamente porque a abstração torna a busca de relações barata de escrever. GORM tem as ferramentas para evitá-lo (Preload, Joins), mas você precisa lembrar de usá-las. Some a isso o overhead de reflexão e a curva mais longa para dominar convenções e a API fluente, e o perfil fica claro: GORM troca um pouco de transparência e performance por muita velocidade de desenvolvimento em domínios ricos em relações.
Quando escolher sqlx
sqlx é a escolha certa quando você quer que o SQL seja explícito e o overhead seja mínimo. Se o time já pensa em SQL, sqlx quase não adiciona conceitos novos: você usa a database/sql de sempre, com o bônus de Get e Select fazerem o scan de linhas direto em structs via tags db. Cada query que toca o banco está no seu código, então revisar performance é ler o que está escrito — não existe query implícita para caçar. Isso torna o N+1 improvável por construção: relações só carregam quando você escreve o JOIN ou a segunda consulta.
package main
import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
type User struct {
ID int `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
func main() {
db, err := sqlx.Connect("postgres", "postgres://localhost/app")
if err != nil {
panic(err)
}
_, err = db.Exec(
`INSERT INTO users (name, email) VALUES ($1, $2)`,
"Ada", "ada@example.com",
)
if err != nil {
panic(err)
}
var users []User
// SQL explícito; Select faz o scan de cada linha em User via tags db
err = db.Select(&users, `SELECT id, name, email FROM users WHERE name = $1`, "Ada")
if err != nil {
panic(err)
}
}
A diferença de filosofia salta aos olhos: não há AutoMigrate (a tabela users precisa existir, criada por SQL ou por uma ferramenta de migração à parte), não há associações e não há hooks. Em troca, você tem previsibilidade total — o SQL que você lê é o SQL que roda, o overhead sobre a database/sql é desprezível, e o modelo mental é curto se você já conhece SQL. O custo aparece quando o domínio tem muitas relações: montar JOINs, mapear resultados aninhados e versionar migrações à mão vira trabalho repetitivo que um ORM automatizaria. sqlx assume que esse trabalho explícito é um preço justo pela transparência.
Veredito
A decisão gira em torno de uma pergunta sobre a forma do seu domínio e a cultura do seu time. Se a aplicação é rica em relações, o CRUD é volumoso e você quer velocidade de desenvolvimento com associações declarativas, migrações automáticas e hooks, GORM se paga — desde que a equipe internalize o hábito de usar Preload/Joins para não cair em N+1 e aceite algum overhead de abstração. É o caminho mais produtivo para domínios densos em entidades relacionadas.
Se o time pensa naturalmente em SQL, valoriza previsibilidade e quer o menor overhead possível sobre a biblioteca padrão, sqlx é a base mais honesta. Cada query é visível, o modelo mental é curto e não há mágica para depurar quando a performance importa — ao custo de escrever migrações, JOINs e mapeamentos à mão. Uma regra prática: quanto mais o gargalo do seu produto for a evolução do schema e a navegação de relações, mais GORM ajuda; quanto mais for o controle fino sobre queries e latência, mais sqlx recompensa. Escolha pela natureza do trabalho, não pela reputação de ORMs.