Quem chega em Go vindo do Django, do Rails, do Laravel ou do Entity Framework sente falta de algo que essas stacks dão de graça: um ORM que mapeia structs para tabelas, cuida de relacionamentos e abstrai o SQL repetitivo. A biblioteca padrão de Go oferece database/sql, excelente e explícito, mas verboso. É aí que entra o GORM, o ORM mais popular do ecossistema Go.
Este guia mostra como usar GORM em produção sem cair nas armadilhas clássicas: models bem declarados, CRUD idiomático, relacionamentos, transações, migrações, o temido problema N+1 e a comparação honesta com abordagens mais explícitas. Ele complementa o guia de database/sql em produção, o tutorial de sqlc para SQL type-safe, o guia de transações e locks no PostgreSQL e o guia de migrations versionadas. Se você está montando uma API completa, leia também o guia de APIs REST em Go.
O que é GORM (e o que não é)
GORM é um ORM (Object-Relational Mapper) para Go. A ideia é simples: você declara uma struct, o GORM mapeia essa struct para uma tabela, e em vez de escrever INSERT INTO users (name, email) VALUES (...) você escreve db.Create(&user). O ganho é produtividade e menos código repetitivo; o custo é uma camada de abstração que esconde o SQL e usa reflection por baixo.
O importante é entender o que GORM não é. Ele não é a única forma de acessar banco em Go, nem a mais “correta”. Go tem uma cultura de explicitude: muitos times preferem sqlc, que gera código Go a partir de queries SQL reais, ou database/sql puro. GORM brilha em domínios com muitos relacionamentos, CRUD administrativo e times que valorizam velocidade de desenvolvimento. Em leituras de alta performance ou em domínios onde o SQL é o contrato, abordagens explícitas costumam ser melhores.
Instalação e conexão
GORM é um módulo por cima do driver do banco. Para PostgreSQL, você combina o GORM com o driver gorm.io/driver/postgres:
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
A conexão recebe uma string DSN e opções como pool máximo e tempo máximo de vida da conexão, exatamente como em database/sql:
package main
import (
"log"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func mustOpenDB(dsn string) *gorm.DB {
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
})
if err != nil {
log.Fatalf("falha ao conectar no banco: %v", err)
}
sqlDB, err := db.DB()
if err != nil {
log.Fatalf("falha ao obter sqlDB: %v", err)
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(25)
sqlDB.SetConnMaxLifetime(5 * time.Minute)
return db
}
Duas observações: o logger do GORM é a sua melhor ferramenta em desenvolvimento — ele imprime cada SQL executado e o tempo gasto, o que ajuda a detectar problemas de performance e N+1. Em produção, suba para logger.Error para não vazar SQL em logs. E os limites do pool (SetMaxOpenConns, SetConnMaxLifetime) seguem as mesmas regras de pool de conexões que você aplicaria em database/sql.
Models, tags e convenções
Um model é uma struct Go com tags do GORM. Por convenção, o nome da struct vira o nome da tabela no plural (User → users), e campos como ID, CreatedAt, UpdatedAt, DeletedAt têm comportamento automático:
package model
import "time"
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:150;uniqueIndex;not null"`
Age int `gorm:"-"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
Pontos-chave dessa declaração: gorm:"size:100" define o tamanho da coluna; uniqueIndex cria um índice único; gorm:"-" faz o GORM ignorar o campo (útil para dados calculados ou de transporte). O campo DeletedAt do tipo gorm.DeletedAt ativa o soft delete: chamadas de Delete não removem a linha, apenas preenchem essa coluna, e Find automaticamente filtra linhas “deletadas”. É conveniente, mas você precisa estar ciente de que os dados continuam no banco.
Se quiser sobrescrever o nome da tabela, implemente a interface Tabler:
func (User) TableName() string {
return "app_users"
}
CRUD sem mistério
As operações básicas seguem verbos naturais. Para criar:
user := model.User{Name: "Ana Souza", Email: "[email protected]"}
if err := db.Create(&user).Error; err != nil {
return err
}
// user.ID agora está preenchido
Para buscar um registro por chave primária ou por condição:
var u model.User
if err := db.First(&u, 42).Error; err != nil {
// err == gorm.ErrRecordNotFound quando não existe
return err
}
if err := db.Where("email = ?", "[email protected]").First(&u).Error; err != nil {
return err
}
Sempre passe valores como parâmetros (?), nunca concatenar strings — isso evita injeção de SQL, o mesmo princípio de segurança que vale para qualquer camada de banco. Para listar vários registros:
var users []model.User
if err := db.Where("age >= ?", 18).Order("created_at desc").Limit(50).Find(&users).Error; err != nil {
return err
}
Para atualizar, prefira selecionar os campos explicitamente para não sobrescrever colunas sem querer:
if err := db.Model(&u).Select("Name", "Age").Updates(map[string]any{
"Name": "Ana Souza Lima",
"Age": 30,
}).Error; err != nil {
return err
}
Para deletar (soft delete, por causa do DeletedAt):
if err := db.Delete(&model.User{}, 42).Error; err != nil {
return err
}
Relacionamentos
GORM cuida de relacionamentos com tags declarativas. Um usuário com muitos posts e um post que pertence a um usuário:
type Post struct {
ID uint `gorm:"primaryKey"`
Title string
Body string
UserID uint
User User `gorm:"foreignKey:UserID"`
Tags []Tag `gorm:"many2many:post_tags;"`
}
type Tag struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex"`
}
O ponto crítico aqui é o carregamento. Se você buscar um post e depois acessar post.User em cada item de uma lista, o GORM fará uma query extra por post — o famoso problema N+1. A solução é o Preload, que carrega o relacionamento em uma única query:
var posts []Post
if err := db.Preload("User").Preload("Tags").Find(&posts).Error; err != nil {
return err
}
O Preload é a diferença entre uma página que faz 1 query e uma que faz 101. Em desenvolvimento, com o logger ativo, você vê o N+1 acontecer em tempo real. Para detalhes avançados de concorrência e cancelamento, lembre-se que o GORM aceita um context.Context via db.WithContext(ctx).
Transações e atomicidade
Transações no GORM são explícitas e seguras, usando uma função que retorna erro. Se a função retornar erro, o GORM faz rollback; se retornar nil, faz commit:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err
}
for _, item := range order.Items {
if err := tx.Create(&item).Error; err != nil {
return err // rollback de tudo
}
}
return nil // commit
})
Isso é mais seguro que gerenciar Begin/Commit/Rollback manualmente, porque cobre também o caso de pânico. Para retries sob contenção ou locks no PostgreSQL, encapsule a chamada em um loop com backoff.
AutoMigrate: útil, mas com cautela
O GORM oferece db.AutoMigrate(&model.User{}), que cria tabelas e adiciona colunas faltantes com base nas structs. É ótimo para protótipos e para subir um ambiente de desenvolvimento rápido:
if err := db.AutoMigrate(&model.User{}, &model.Post{}, &model.Tag{}); err != nil {
log.Fatalf("auto migrate falhou: %v", err)
}
Em produção, porém, o AutoMigrate não é a forma recomendada de versionar o schema. Ele só adiciona colunas, não remove nem renomeia, não rastreia estado entre ambientes e pode divergir entre máquinas. Para produção, use migrations versionadas e explícitas, que garantem que laptop, CI, staging e produção saibam exatamente qual versão do banco está rodando. Pense no AutoMigrate como um atalho de desenvolvimento, não como estratégia de schema.
Hooks e callbacks
GORM permite executar lógica antes ou depois de operações via métodos com nomes reservados, como BeforeCreate:
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Email == "" {
return errors.New("email é obrigatório")
}
return nil
}
Hooks são poderosos, mas cuidado: lógica escondida em hooks torna o fluxo difícil de seguir e de testar. Para validação de negócio, muitas vezes é mais claro validar explicitamente antes de chamar db.Create, mantendo o model como um contêiner de dados e a validação no domínio.
GORM vs sqlc vs database/sql
A decisão de camada de banco depende do domínio e do time. A tabela resume quando cada abordagem brilha:
| Critério | GORM | sqlc | database/sql |
|---|---|---|---|
| Velocidade de desenvolvimento | Alta | Média | Baixa |
| Controle sobre o SQL | Baixo | Total | Total |
| Type safety de queries | Parcial | Total (gerada) | Manual |
| Relacionamentos complexos | Fácil | Verboso | Verboso |
| Reflexão / custo de runtime | Sim | Não | Não |
| Boa para CRUD administrativo | Sim | Aceitável | Aceitável |
| Boa para leituras críticas | Com cuidado | Sim | Sim |
Uma combinação comum e saudável é sqlc para leituras críticas e GORM para CRUD administrativo, dentro do mesmo projeto. Não existe “a forma certa” universal — existe a forma certa para o seu domínio. O pior erro é misturar as três sem critério e sem documentar a fronteira entre elas.
Armadilhas comuns
Três problemas aparecem com frequência em projetos com GORM:
- N+1 — sempre que percorrer uma lista e acessar um relacionamento, use
Preload. Confirme com o logger ligado. - Soft delete inesperado — o
DeletedAtfazFindesconder linhas. Se precisar de uma contagem real ou de uma listagem de tudo, useUnscoped().Find(...). - Updates amplos demais —
Saveatualiza todas as colunas;UpdatescomSelecté mais seguro quando você quer mudar só alguns campos.
Para diagnosticar problemas de performance em produção, combine o logger do GORM com o guia de pprof. Geralmente o gargalo não é o GORM em si, mas o padrão de queries que ele facilita.
Se você trabalha em times com várias linguagens, vale comparar com o jeito que o Python Dev Brasil trata bancos de dados com SQLAlchemy: os tradeoffs entre ORM e SQL explícito são os mesmos em qualquer stack.
Perguntas frequentes
GORM é a melhor escolha para todo projeto Go?
Não. GORM é excelente para produtividade em domínios com muitos relacionamentos e CRUD intensivo, mas em serviços de alta performance ou onde o SQL é o contrato, sqlc ou database/sql costumam ser melhores. Avalie por domínio e por time, não por hype.
Como faço queries raw quando preciso?
Use db.Raw("SELECT ... WHERE id = ?", id).Scan(&result) para leituras e db.Exec("UPDATE ... WHERE id = ?", id) para escritas. Você mantém a conexão e o pool do GORM, mas escreve o SQL explicitamente quando a abstração atrapalha.
GORM funciona com PostgreSQL, MySQL e SQLite?
Sim. GORM usa drivers separados (gorm.io/driver/postgres, gorm.io/driver/mysql, gorm.io/driver/sqlite). A API é a mesma; o que muda é o Dialector e pequenas diferenças de SQL específicas de cada banco. Para PostgreSQL em produção com pgxpool, o driver do GORM já usa pgx por baixo.
Como testo código que usa GORM?
Use SQLite em memória (sqlite.Open(":memory:")) ou Testcontainers para um banco real em testes de integração. Evite mockar o GORM — teste contra um banco de verdade para confiar no resultado. Veja o guia de mocks, fakes e httptest para a fronteira entre o que mockar e o que testar de verdade.
Conclusão
GORM é a porta de entrada natural para quem quer produtividade de ORM em Go. Usado com disciplina — Preload para evitar N+1, Select para updates controlados, migrations versionadas em produção e queries raw quando a abstração atrapalha — ele acelera muito o desenvolvimento sem sacrificar a clareza. A chave é saber a fronteira entre GORM, sqlc e database/sql, e escolher cada um onde brilha.
Para continuar aprendendo, explore o roadmap de Go para 2026, confira vagas de Go no Brasil e entenda a faixa de salários de desenvolvedor Go no mercado brasileiro. O próximo passo depois de dominar a camada de banco é estruturar a aplicação inteira — leia o guia de clean architecture em Go para amarrar models, repositórios e casos de uso sem overengineering.