← Voltar para o blog

GORM em Go: O ORM Mais Popular do Ecossistema Explicado

GORM em Go na prática: models, CRUD, relacionamentos, transações, AutoMigrate, N+1, hooks e quando usar GORM em vez de sqlc ou database/sql. Com exemplos.

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 (Userusers), 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érioGORMsqlcdatabase/sql
Velocidade de desenvolvimentoAltaMédiaBaixa
Controle sobre o SQLBaixoTotalTotal
Type safety de queriesParcialTotal (gerada)Manual
Relacionamentos complexosFácilVerbosoVerboso
Reflexão / custo de runtimeSimNãoNão
Boa para CRUD administrativoSimAceitávelAceitável
Boa para leituras críticasCom cuidadoSimSim

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:

  1. N+1 — sempre que percorrer uma lista e acessar um relacionamento, use Preload. Confirme com o logger ligado.
  2. Soft delete inesperado — o DeletedAt faz Find esconder linhas. Se precisar de uma contagem real ou de uma listagem de tudo, use Unscoped().Find(...).
  3. Updates amplos demaisSave atualiza todas as colunas; Updates com Select é 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.