SQLite é o banco de dados mais implantado do mundo. Está presente em navegadores, sistemas operacionais, dispositivos móveis, aplicativos desktop e em bilhões de dispositivos embarcados. Diferente de MySQL e PostgreSQL, ele não é um servidor separado — é uma biblioteca que a aplicação carrega diretamente, armazenando tudo em um único arquivo no disco.
Para Go, isso significa zero infraestrutura: sem Docker, sem processo separado, sem configuração de rede. O banco sobe junto com a aplicação e encerra junto com ela. Essa característica o torna ideal para CLIs, ferramentas de desenvolvimento, aplicações desktop, testes de integração e serviços com volume moderado de dados.
Os dois drivers disponíveis
Go tem dois drivers principais para SQLite, com filosofias diferentes:
mattn/go-sqlite3 — usa CGo para vincular a biblioteca C do SQLite. Performance excelente e total compatibilidade com a biblioteca oficial. Requer um compilador C (GCC ou Clang) na máquina de build e no ambiente de CI.
modernc.org/sqlite — port puro de Go do SQLite, transpilado automaticamente do código C original. Não requer CGo nem compilador C. Ligeiramente mais lento, mas sem dependências externas de build.
Para a maioria dos projetos, modernc.org/sqlite é a escolha mais prática por eliminar a dependência do compilador C:
# Driver puro Go — recomendado para a maioria dos casos
go get modernc.org/sqlite
# Driver CGo — melhor performance, requer GCC
go get github.com/mattn/go-sqlite3
Conexão e configuração
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "modernc.org/sqlite"
)
func abrirSQLite(caminho string) (*sql.DB, error) {
// DSN do SQLite: caminho do arquivo ou ":memory:" para banco em memória
dsn := fmt.Sprintf("%s?_journal_mode=WAL&_foreign_keys=on&_busy_timeout=5000", caminho)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("abrir SQLite: %w", err)
}
// SQLite não suporta múltiplas conexões de escrita simultâneas
// Uma única conexão de escrita é o padrão recomendado
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
db.SetConnMaxLifetime(0) // conexões vivem para sempre
ctx, cancelar := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelar()
if err := db.PingContext(ctx); err != nil {
db.Close()
return nil, fmt.Errorf("verificar conexão: %w", err)
}
log.Printf("SQLite aberto: %s", caminho)
return db, nil
}
func main() {
// Banco em arquivo
db, err := abrirSQLite("dados.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
fmt.Println("SQLite pronto")
}
Os parâmetros do DSN merecem atenção:
_journal_mode=WAL— Write-Ahead Logging permite leituras concorrentes enquanto uma escrita está em andamento, melhorando significativamente a performance_foreign_keys=on— chaves estrangeiras são desabilitadas por padrão no SQLite; este parâmetro as ativa_busy_timeout=5000— espera até 5 segundos quando o banco está locked, em vez de retornar erro imediatamente
Banco em memória para testes
Uma das vantagens mais valiosas do SQLite é a possibilidade de criar bancos completamente em memória — perfeitos para testes de integração que precisam de um banco real sem infraestrutura:
func abrirSQLiteMemoria() (*sql.DB, error) {
// ":memory:" cria um banco que existe apenas na RAM
// "file::memory:?cache=shared" permite múltiplas conexões ao mesmo banco em memória
db, err := sql.Open("sqlite", "file::memory:?cache=shared&_foreign_keys=on")
if err != nil {
return nil, err
}
db.SetMaxOpenConns(1)
return db, db.Ping()
}
Atenção ao cache=shared: ele faz todos os sql.Open do processo com esse mesmo DSN enxergarem o mesmo banco em memória — num teste, um segundo sql.Open viu a tabela criada pelo primeiro. Com testes rodando em paralelo, um contamina o outro. Para isolar, dê um nome diferente a cada banco: fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()).
Schema e migrações
package main
import (
"context"
"database/sql"
"fmt"
)
const schemaPrincipal = `
CREATE TABLE IF NOT EXISTS categorias (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
categoria_id INTEGER REFERENCES categorias(id),
titulo TEXT NOT NULL,
descricao TEXT,
concluida BOOLEAN NOT NULL DEFAULT 0,
prioridade INTEGER NOT NULL DEFAULT 0 CHECK(prioridade BETWEEN 0 AND 3),
prazo DATETIME,
criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
atualizado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS tarefas_tags (
tarefa_id INTEGER NOT NULL REFERENCES tarefas(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (tarefa_id, tag_id)
);
CREATE INDEX IF NOT EXISTS idx_tarefas_categoria ON tarefas(categoria_id);
CREATE INDEX IF NOT EXISTS idx_tarefas_concluida ON tarefas(concluida);
CREATE INDEX IF NOT EXISTS idx_tarefas_prazo ON tarefas(prazo);
`
func inicializarSchema(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, schemaPrincipal)
if err != nil {
return fmt.Errorf("inicializar schema: %w", err)
}
return nil
}
Tipos em SQLite: o sistema de afinidades
SQLite usa um sistema de tipos diferente dos demais bancos. Em vez de tipos rígidos, usa afinidades de tipo:
| Afinidade SQLite | Tipos declarados | Tipo Go |
|---|---|---|
| INTEGER | INT, INTEGER, BIGINT | int64, bool |
| REAL | REAL, FLOAT, DOUBLE | float64 |
| TEXT | TEXT, VARCHAR, CHAR | string, time.Time |
| BLOB | BLOB | []byte |
| NUMERIC | DECIMAL, NUMERIC | string (para precisão) |
time.Time é armazenado como TEXT no formato RFC3339 ou como INTEGER (Unix timestamp). A conversão precisa ser manual:
type Tarefa struct {
ID int64
CategoriaID sql.NullInt64
Titulo string
Descricao sql.NullString
Concluida bool
Prioridade int
Prazo sql.NullTime
CriadoEm time.Time
AtualizadoEm time.Time
}
Repositório completo de tarefas
package main
import (
"context"
"database/sql"
"fmt"
"time"
)
type TarefaInput struct {
CategoriaID *int64
Titulo string
Descricao string
Prioridade int
Prazo *time.Time
}
type RepoTarefas struct {
db *sql.DB
}
func NovoRepoTarefas(db *sql.DB) *RepoTarefas {
return &RepoTarefas{db: db}
}
func (r *RepoTarefas) Criar(ctx context.Context, input TarefaInput) (*Tarefa, error) {
query := `
INSERT INTO tarefas (categoria_id, titulo, descricao, prioridade, prazo)
VALUES (?, ?, ?, ?, ?)
RETURNING id, categoria_id, titulo, descricao, concluida,
prioridade, prazo, criado_em, atualizado_em
`
var catID sql.NullInt64
if input.CategoriaID != nil {
catID = sql.NullInt64{Int64: *input.CategoriaID, Valid: true}
}
var desc sql.NullString
if input.Descricao != "" {
desc = sql.NullString{String: input.Descricao, Valid: true}
}
var prazo sql.NullTime
if input.Prazo != nil {
prazo = sql.NullTime{Time: *input.Prazo, Valid: true}
}
var t Tarefa
err := r.db.QueryRowContext(ctx, query,
catID, input.Titulo, desc, input.Prioridade, prazo,
).Scan(
&t.ID, &t.CategoriaID, &t.Titulo, &t.Descricao,
&t.Concluida, &t.Prioridade, &t.Prazo,
&t.CriadoEm, &t.AtualizadoEm,
)
if err != nil {
return nil, fmt.Errorf("criar tarefa: %w", err)
}
return &t, nil
}
func (r *RepoTarefas) BuscarPorID(ctx context.Context, id int64) (*Tarefa, error) {
query := `
SELECT id, categoria_id, titulo, descricao, concluida,
prioridade, prazo, criado_em, atualizado_em
FROM tarefas
WHERE id = ?
`
var t Tarefa
err := r.db.QueryRowContext(ctx, query, id).Scan(
&t.ID, &t.CategoriaID, &t.Titulo, &t.Descricao,
&t.Concluida, &t.Prioridade, &t.Prazo,
&t.CriadoEm, &t.AtualizadoEm,
)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("tarefa %d não encontrada", id)
}
if err != nil {
return nil, fmt.Errorf("buscar tarefa: %w", err)
}
return &t, nil
}
type FiltroTarefas struct {
Concluida *bool
CategoriaID *int64
Prioridade *int
Limite int
Offset int
}
func (r *RepoTarefas) Listar(ctx context.Context, filtro FiltroTarefas) ([]Tarefa, error) {
// Construção dinâmica de query com filtros opcionais
base := `
SELECT id, categoria_id, titulo, descricao, concluida,
prioridade, prazo, criado_em, atualizado_em
FROM tarefas
WHERE 1=1
`
args := []any{}
if filtro.Concluida != nil {
base += " AND concluida = ?"
args = append(args, *filtro.Concluida)
}
if filtro.CategoriaID != nil {
base += " AND categoria_id = ?"
args = append(args, *filtro.CategoriaID)
}
if filtro.Prioridade != nil {
base += " AND prioridade = ?"
args = append(args, *filtro.Prioridade)
}
base += " ORDER BY prioridade DESC, criado_em DESC"
if filtro.Limite > 0 {
base += " LIMIT ?"
args = append(args, filtro.Limite)
}
if filtro.Offset > 0 {
base += " OFFSET ?"
args = append(args, filtro.Offset)
}
rows, err := r.db.QueryContext(ctx, base, args...)
if err != nil {
return nil, fmt.Errorf("listar tarefas: %w", err)
}
defer rows.Close()
var tarefas []Tarefa
for rows.Next() {
var t Tarefa
if err := rows.Scan(
&t.ID, &t.CategoriaID, &t.Titulo, &t.Descricao,
&t.Concluida, &t.Prioridade, &t.Prazo,
&t.CriadoEm, &t.AtualizadoEm,
); err != nil {
return nil, fmt.Errorf("scan tarefa: %w", err)
}
tarefas = append(tarefas, t)
}
return tarefas, rows.Err()
}
func (r *RepoTarefas) Concluir(ctx context.Context, id int64) error {
resultado, err := r.db.ExecContext(ctx,
"UPDATE tarefas SET concluida = 1, atualizado_em = CURRENT_TIMESTAMP WHERE id = ?",
id,
)
if err != nil {
return fmt.Errorf("concluir tarefa: %w", err)
}
if n, _ := resultado.RowsAffected(); n == 0 {
return fmt.Errorf("tarefa %d não encontrada", id)
}
return nil
}
func (r *RepoTarefas) Deletar(ctx context.Context, id int64) error {
resultado, err := r.db.ExecContext(ctx,
"DELETE FROM tarefas WHERE id = ?", id)
if err != nil {
return fmt.Errorf("deletar tarefa: %w", err)
}
if n, _ := resultado.RowsAffected(); n == 0 {
return fmt.Errorf("tarefa %d não encontrada", id)
}
return nil
}
Relacionamento muitos-para-muitos: tags
func (r *RepoTarefas) AdicionarTag(ctx context.Context, tarefaID int64, nomeTag string) error {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
}
}()
// Insere a tag se não existir (INSERT OR IGNORE)
_, err = tx.ExecContext(ctx,
"INSERT OR IGNORE INTO tags (nome) VALUES (?)", nomeTag)
if err != nil {
return fmt.Errorf("inserir tag: %w", err)
}
// Obtém o ID da tag
var tagID int64
err = tx.QueryRowContext(ctx,
"SELECT id FROM tags WHERE nome = ?", nomeTag).Scan(&tagID)
if err != nil {
return fmt.Errorf("buscar tag: %w", err)
}
// Associa tag à tarefa (INSERT OR IGNORE evita duplicatas)
_, err = tx.ExecContext(ctx,
"INSERT OR IGNORE INTO tarefas_tags (tarefa_id, tag_id) VALUES (?, ?)",
tarefaID, tagID,
)
if err != nil {
return fmt.Errorf("associar tag: %w", err)
}
return tx.Commit()
}
type TarefaComTags struct {
Tarefa
Tags []string
}
func (r *RepoTarefas) BuscarComTags(ctx context.Context, id int64) (*TarefaComTags, error) {
tarefa, err := r.BuscarPorID(ctx, id)
if err != nil {
return nil, err
}
rows, err := r.db.QueryContext(ctx, `
SELECT t.nome
FROM tags t
INNER JOIN tarefas_tags tt ON tt.tag_id = t.id
WHERE tt.tarefa_id = ?
ORDER BY t.nome
`, id)
if err != nil {
return nil, fmt.Errorf("buscar tags: %w", err)
}
defer rows.Close()
resultado := &TarefaComTags{Tarefa: *tarefa}
for rows.Next() {
var tag string
if err := rows.Scan(&tag); err != nil {
return nil, err
}
resultado.Tags = append(resultado.Tags, tag)
}
return resultado, rows.Err()
}
Testes de integração com banco em memória
package main
import (
"context"
"database/sql"
"testing"
_ "modernc.org/sqlite"
)
func configurarBancoDeTeste(t *testing.T) *sql.DB {
t.Helper()
db, err := abrirSQLiteMemoria()
if err != nil {
t.Fatalf("abrir banco de teste: %v", err)
}
ctx := context.Background()
if err := inicializarSchema(ctx, db); err != nil {
db.Close()
t.Fatalf("inicializar schema: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}
func TestRepoTarefas_CriarEBuscar(t *testing.T) {
db := configurarBancoDeTeste(t)
repo := NovoRepoTarefas(db)
ctx := context.Background()
input := TarefaInput{
Titulo: "Estudar Go",
Descricao: "Completar o módulo de banco de dados",
Prioridade: 2,
}
criada, err := repo.Criar(ctx, input)
if err != nil {
t.Fatalf("criar tarefa: %v", err)
}
if criada.ID == 0 {
t.Error("ID não foi gerado")
}
if criada.Titulo != input.Titulo {
t.Errorf("título esperado '%s', obtido '%s'", input.Titulo, criada.Titulo)
}
buscada, err := repo.BuscarPorID(ctx, criada.ID)
if err != nil {
t.Fatalf("buscar tarefa: %v", err)
}
if buscada.Titulo != criada.Titulo {
t.Errorf("títulos não conferem: '%s' vs '%s'",
criada.Titulo, buscada.Titulo)
}
}
func TestRepoTarefas_Concluir(t *testing.T) {
db := configurarBancoDeTeste(t)
repo := NovoRepoTarefas(db)
ctx := context.Background()
tarefa, _ := repo.Criar(ctx, TarefaInput{Titulo: "Tarefa de teste"})
if err := repo.Concluir(ctx, tarefa.ID); err != nil {
t.Fatalf("concluir tarefa: %v", err)
}
atualizada, _ := repo.BuscarPorID(ctx, tarefa.ID)
if !atualizada.Concluida {
t.Error("tarefa deveria estar marcada como concluída")
}
}
func TestRepoTarefas_NaoEncontrada(t *testing.T) {
db := configurarBancoDeTeste(t)
repo := NovoRepoTarefas(db)
ctx := context.Background()
_, err := repo.BuscarPorID(ctx, 99999)
if err == nil {
t.Error("esperava erro para ID inexistente")
}
}
Backup e manutenção
func fazerBackup(ctx context.Context, db *sql.DB, destino string) error {
// O método mais simples: usar o comando VACUUM INTO
_, err := db.ExecContext(ctx, "VACUUM INTO ?", destino)
if err != nil {
return fmt.Errorf("backup: %w", err)
}
return nil
}
func otimizarBanco(ctx context.Context, db *sql.DB) error {
comandos := []string{
"PRAGMA optimize", // atualiza estatísticas do query planner
"PRAGMA wal_checkpoint(TRUNCATE)", // consolida WAL
"VACUUM", // compacta o arquivo do banco
"ANALYZE", // atualiza estatísticas de índices
}
for _, cmd := range comandos {
if _, err := db.ExecContext(ctx, cmd); err != nil {
return fmt.Errorf("otimizar (%s): %w", cmd, err)
}
}
return nil
}
Limitações do SQLite a conhecer
SQLite é excelente para muitos casos, mas tem limitações importantes:
Concorrência de escrita. SQLite suporta múltiplos leitores simultâneos, mas apenas um escritor por vez. Para aplicações com alta carga de escrita concorrente, MySQL ou PostgreSQL são mais adequados.
Tipos de dados. O sistema de afinidades é flexível, mas pode surpreender. Um campo INTEGER comum aceita texto sem erro. Desde a versão 3.37 (2021), tabelas declaradas com STRICT no fim do CREATE TABLE passam a recusar o valor do tipo errado, com cannot store TEXT value in INTEGER column — para projetos novos, é o modo recomendado.
ALTER TABLE limitado. O SQLite não suporta todas as formas de ALTER TABLE. Adicionar colunas sempre funcionou; renomear coluna existe desde a versão 3.25 (2018) e remover coluna desde a 3.35 (2021) — o SQLite 3.53 embutido no modernc.org/sqlite atual aceita as duas. Mudar o tipo de uma coluna ou alterar restrições continua exigindo recriar a tabela.
Sem suporte a funções de janela em versões antigas. A partir do SQLite 3.25 (2018), funções de janela são suportadas — mas versões antigas do sistema operacional podem ter versões mais antigas do SQLite.
O SQLite troca o servidor por um arquivo, e com isso some quase toda a infraestrutura — e aparecem cuidados próprios. O modo WAL e o busy_timeout tornam a concorrência aceitável; STRICT traz de volta a verificação de tipos; VACUUM INTO é o backup seguro, porque o arquivo sozinho não contém o que está no WAL. Entre os drivers, o modernc.org/sqlite mantém o binário estático e o mattn/go-sqlite3 exige cgo em todo build. Para testes, o banco em memória é imbatível, desde que cada teste tenha o seu.
Fontes e leituras recomendadas
-
modernc.org/sqlite — Driver SQLite puro Go, sem CGo. https://pkg.go.dev/modernc.org/sqlite
-
mattn/go-sqlite3 — Driver SQLite com CGo, máxima compatibilidade. https://github.com/mattn/go-sqlite3
-
Documentação oficial SQLite — Referência completa da linguagem e pragmas. https://www.sqlite.org/docs.html
-
SQLite WAL mode — Explicação detalhada do Write-Ahead Logging. https://www.sqlite.org/wal.html
-
SQLite em aplicações de produção — Artigo de Litestream sobre uso em produção. https://litestream.io/
-
Litestream — Ferramenta de replicação contínua para SQLite em produção. https://litestream.io
Exercícios
Exercício 1
Um script de backup noturno copia o arquivo dados.db com cp enquanto a aplicação está rodando com journal_mode=WAL. Num dia de restauração, o backup abre, mas faltam os dados das últimas horas, ou até tabelas inteiras. O que o cp deixou para trás, e qual é a forma certa de copiar?
Ver resposta
✓ Resposta: No modo WAL, as escritas novas vão primeiro para o arquivo dados.db-wal, e só são transferidas para o dados.db nos checkpoints, que o SQLite faz periodicamente. Copiar apenas o .db captura o estado do último checkpoint, sem nada do que está no WAL. Num teste com uma tabela criada e 100 linhas inseridas em modo WAL, a cópia do .db feita logo em seguida abriu sem a tabela: a consulta devolveu no such table: t, porque tudo ainda estava no -wal. Copiar os dois arquivos juntos também não é seguro, porque a aplicação pode escrever entre as duas cópias. A forma certa é pedir ao próprio SQLite: VACUUM INTO 'backup.db', mostrado no artigo, gera uma cópia consistente e compacta; no mesmo teste, ela saiu com as 100 linhas. A alternativa é a API de backup online do SQLite, disponível pelos drivers. E, como qualquer backup, esse precisa de restauração testada periodicamente, porque o defeito do cp só aparece no dia em que o backup é necessário.
Exercício 2
Um time troca o modernc.org/sqlite pelo mattn/go-sqlite3 em busca de desempenho. O build do CI, que usa CGO_ENABLED=0 para gerar binários estáticos, continua passando, mas o serviço cai ao subir. Qual é a mensagem, e por que o problema não apareceu na compilação?
Ver resposta
✓ Resposta: O mattn/go-sqlite3 é um invólucro da biblioteca C do SQLite e precisa do cgo para funcionar. Quando compilado com CGO_ENABLED=0, ele não falha: usa uma versão de fachada, que compila normalmente e só reclama quando é usada. Num teste, o programa compilou e, no primeiro Ping, devolveu Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub. Como o erro só aparece em tempo de execução, o CI verde não protege nada, a menos que haja um teste que abra o banco com o binário final. As saídas são três: ligar o cgo no build, com um compilador C no CI e, em geral, abrir mão do binário estático, porque a libc passa a ser dependência; ficar com o modernc.org/sqlite, que é Go puro e funciona com CGO_ENABLED=0; ou medir antes de trocar. A diferença de desempenho entre os dois raramente é o gargalo de uma aplicação que usa SQLite, e o custo operacional do cgo é certo.
Exercício 3
O artigo configura o SQLite com db.SetMaxOpenConns(1). Um desenvolvedor escreve um método que abre uma transação com r.db.BeginTx e, dentro dela, chama r.BuscarPorID(ctx, id), que usa r.db e não a transação. O programa trava para sempre nessa linha. Por quê?
Ver resposta
✓ Resposta: Com uma única conexão no pool, a transação aberta está segurando essa conexão até o Commit ou o Rollback. O BuscarPorID, por usar r.db, pede uma conexão ao pool, e o pool, que não pode abrir uma segunda, fica esperando a primeira ser liberada. Só que a primeira só será liberada quando o método terminar, e o método está esperando o BuscarPorID: é um impasse dentro da mesma goroutine, sem erro nenhum, a menos que o contexto tenha prazo. É o mesmo mecanismo medido no artigo sobre database/sql, em que um rows não fechado com pool de uma conexão fez a consulta seguinte esperar até o prazo. A correção é fazer as consultas feitas dentro de uma transação usarem a própria transação: o padrão habitual é os métodos do repositório receberem uma interface com os métodos comuns a *sql.DB e *sql.Tx, como QueryRowContext e ExecContext, e o chamador decidir qual passar. Esse cuidado vale para qualquer banco; com o SQLite e uma conexão só, o defeito apenas aparece mais cedo.
Exercício 4
Uma coluna prioridade INTEGER passa a receber, por um erro no front-end, valores como "alta". O SQLite grava sem reclamar, e o Scan para int começa a falhar dias depois, na hora de listar. Como o SQLite permite isso, e como fazer o banco recusar na entrada?
Ver resposta
✓ Resposta: No sistema de afinidades do SQLite, o tipo declarado da coluna é uma preferência, e não uma regra: ao receber um texto que não parece número, uma coluna INTEGER comum simplesmente o guarda como texto. Num teste, o INSERT de 'texto' numa coluna INTEGER foi aceito sem erro. O defeito só aparece na leitura, quando o Scan tenta converter "alta" em int, longe do lugar e do momento em que o dado ruim entrou. Desde a versão 3.37, a palavra STRICT no fim do CREATE TABLE faz o SQLite verificar os tipos como os outros bancos: na mesma medição, a tabela STRICT recusou o valor com cannot store TEXT value in INTEGER column. Para tabelas existentes, uma restrição CHECK(typeof(prioridade) = 'integer') tem efeito parecido, e o CHECK(prioridade BETWEEN 0 AND 3) do próprio artigo já recusa textos, porque a comparação falha. A validação no Go continua necessária, mas o banco passa a ser a última barreira, e não um depósito que aceita tudo.
Exercício 5
A suíte de testes usa abrirSQLiteMemoria, com o DSN file::memory:?cache=shared. Tudo passa rodando em sequência. Quando o time acrescenta t.Parallel() para acelerar, testes começam a falhar de forma intermitente, encontrando registros que não criaram. Explique e corrija.
Ver resposta
✓ Resposta: O cache=shared com o nome :memory: faz todas as conexões do processo que usam esse mesmo DSN enxergarem o mesmo banco em memória. Num teste, um segundo sql.Open com o DSN idêntico viu a tabela e a linha criadas pelo primeiro. Em sequência, cada teste fecha o seu *sql.DB no t.Cleanup antes de o próximo abrir, o banco some quando a última conexão fecha, e o problema não aparece. Em paralelo, vários testes ficam abertos ao mesmo tempo sobre o mesmo banco, e um encontra as tarefas do outro: contagens erradas, IDs inesperados, um CREATE TABLE que falha porque a tabela já existe. A correção é dar a cada teste um banco com nome próprio, fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name()), o que mantém o compartilhamento entre as conexões de um mesmo teste e isola os testes entre si. O nome do teste já é único na suíte; com subtestes, t.Name() inclui o caminho completo, como TestRepo/criar.