Skip to main content


Introdução a banco de dados​


Um banco de dados é usado para armazenar, organizar e recuperar informações que uma aplicação precisa manter salva. Quando desenvolvemos uma aplicação em Go, podemos guardar informações em variáveis, structs, slices e maps. Isso funciona enquanto o programa está executando, mas os dados armazenados apenas na memória normalmente são perdidos quando a aplicação é encerrada.


Um banco de dados permite guardar essas informações de forma persistente, ou seja, os dados podem continuar disponíveis mesmo depois que o programa é fechado e executado novamente. Por exemplo, imagine um sistema de cadastro de produtos. Cada produto possui um nome, um preço e um identificador. Quando um produto é cadastrado, precisamos guardar suas informações para que seja possível consultá-las, alterá-las ou removê-las posteriormente. É justamente nesse tipo de situação que um banco de dados se torna útil.



Como os dados são organizados?​


A forma de organização depende do tipo de banco usado. Nos bancos de dados relacionais, como SQLite, PostgreSQL e MySQL, as informações normalmente são organizadas em tabelas. Uma tabela pode ser entendida como uma estrutura formada por linhas e colunas. Imagine uma tabela chamada usuarios.

idnomeemail
1Joãojoao@email.com
2Mariamaria@email.com
3Pedropedro@email.com

Nessa tabela, temos três conceitos básicos.

  • Tabela

    É a estrutura responsável por armazenar um conjunto de informações relacionadas. Nesse caso, a tabela usuarios guarda os dados dos usuários cadastrados.

  • Coluna

    Representa uma informação específica que faz parte de cada registro. No exemplo, temos as colunas id, nome e email.

  • Linha ou registro

    Representa um conjunto de valores armazenados na tabela. A primeira linha, por exemplo, representa o usuário João, que possui o identificador 1 e o e-mail joao@email.com.


Essa organização facilita o armazenamento e a consulta das informações.



Como podemos consultar as informações?​


Os bancos de dados relacionais normalmente usam uma linguagem chamada SQL (Structured Query Language). SQL é uma linguagem usada para trabalhar com dados armazenados em bancos relacionais. Com ela, podemos cadastrar, consultar, atualizar e remover informações.


Imagine que precisamos buscar todos os usuários cadastrados. Uma consulta SQL poderia ser escrita assim.

-- Seleciona todas as colunas da tabela usuarios
SELECT * FROM usuarios;

O comando SELECT é usado para consultar informações. O símbolo * indica que queremos retornar todas as colunas da tabela. Já FROM usuarios indica de qual tabela os dados serão consultados. Também podemos buscar informações específicas.

-- Retorna apenas o nome e o email dos usuários
SELECT nome, email FROM usuarios;

Nesse segundo exemplo, estamos solicitando apenas as colunas nome e email. O resultado seria semelhante a este.

nomeemail
Joãojoao@email.com
Mariamaria@email.com
Pedropedro@email.com

Ainda não precisamos nos preocupar em executar essas consultas. O objetivo é entender que o banco disponibiliza mecanismos para encontrar informações sem precisar percorrer manualmente todos os registros dentro da aplicação Go.


Também podemos armazenar informações em arquivos JSON, CSV ou arquivos de texto, isso pode até funcionar bem em aplicações simples. O problema começa quando precisamos trabalhar com muitos registros ou realizar operações mais complexas. Imagine um sistema que possui 100 mil usuários. Se todos estiverem armazenados em um único arquivo JSON, talvez seja necessário carregar e processar uma grande quantidade de informações apenas para encontrar determinado usuário.


Além disso, precisamos lidar com outros problemas, como alterações simultâneas, integridade dos dados e gravações interrompidas. Um banco de dados oferece mecanismos próprios para lidar com essas situações, dependendo do banco usado. Por exemplo, podemos solicitar diretamente os dados de um usuário pelo identificador.

-- Busca o usuário que possui o ID igual a 2
SELECT * FROM usuarios WHERE id = 2;

A palavra WHERE é usada para estabelecer uma condição. Nesse caso, estamos solicitando somente o usuário cujo id seja igual a 2. O banco é responsável por executar a consulta e localizar os dados. Dependendo da estrutura usada, como a existência de índices, essa busca pode ser feita sem examinar todos os registros.



Diferença entre banco relacional e não relacional​


Como já vimos, um banco de dados relacional organiza as informações em tabelas, que possuem linhas e colunas. Cada tabela representa um conjunto de informações relacionadas, como usuários, produtos ou pedidos. Uma característica importante desse modelo é a possibilidade de estabelecer relacionamentos entre tabelas, permitindo que diferentes informações sejam conectadas sem precisar repetir todos os dados.


Imagine um sistema que armazena usuários e seus pedidos.

Tabela usuarios

idnomeemail
1Joãojoao@email.com
2Mariamaria@email.com

Tabela pedidos

idusuario_idvalor
11150.00
2180.00
32200.00

A tabela usuarios armazena as informações dos usuários, enquanto a tabela pedidos armazena os pedidos realizados. Observe que a coluna usuario_id, presente na tabela pedidos, indica a qual usuário cada pedido pertence. Nesse exemplo, o usuário João possui dois pedidos, enquanto Maria possui apenas um.


Esse relacionamento pode ser controlado por uma chave estrangeira (foreign key), que estabelece uma referência entre registros de tabelas diferentes. Isso permite manter os dados separados, mas relacionados.


Um banco de dados não relacional trabalha com modelos de armazenamento que não dependem necessariamente da organização tradicional em tabelas relacionadas. Esses bancos também são conhecidos como NoSQL, embora isso não signifique que todos eles sejam incapazes de trabalhar com SQL. Existem diferentes tipos de bancos não relacionais, como bancos orientados a documentos, chave-valor e grafos.


Um exemplo bastante conhecido é o MongoDB, que trabalha com documentos. Em vez de organizar todas as informações em tabelas com uma estrutura fixa, o MongoDB permite armazenar registros em documentos com campos e valores. Esses documentos possuem uma estrutura semelhante ao JSON, um formato bastante comum para representar dados em aplicações.


A principal diferença está no modelo usado para organizar e relacionar as informações. Nos bancos relacionais, normalmente definimos tabelas com colunas e relacionamentos bem estabelecidos. Nos bancos orientados a documentos, como MongoDB, podemos trabalhar com estruturas mais flexíveis, em que os documentos não precisam possuir exatamente os mesmos campos.


Por exemplo, um documento poderia representar um usuário com nome e e-mail, enquanto outro poderia possuir nome, e-mail, telefone e endereço. Isso não significa que bancos não relacionais não possuem regras de validação ou relacionamentos. Esses recursos também podem existir, dependendo do banco escolhido. A diferença é que o modelo relacional oferece uma estrutura baseada em tabelas e relacionamentos definidos, enquanto os bancos não relacionais oferecem outras formas de organizar os dados.



Quando usar SQLite, PostgreSQL ou MySQL​


O SQLite, PostgreSQL e MySQL são bancos de dados relacionais que trabalham com SQL, mas possuem diferenças na arquitetura, na configuração e na maneira como as aplicações acessam as informações. Os três podem ser usados com Go, desde que tenhamos um driver compatível.


A escolha entre eles depende principalmente das necessidades da aplicação e de como o banco será executado.



SQLite​


O SQLite é um banco de dados relacional que funciona diretamente dentro da aplicação, sem precisar de um servidor de banco de dados separado. Essa característica é conhecida como banco embarcado (embedded database). Diferentemente de PostgreSQL e MySQL, que normalmente executam como serviços separados, o SQLite acessa diretamente um arquivo de banco de dados. Por exemplo, podemos ter um projeto Go com a seguinte estrutura.

meu-projeto/
main.go
go.mod
dados.db

O arquivo dados.db representa o banco SQLite, que pode armazenar tabelas, registros, índices e outras informações. A aplicação Go acessa esse arquivo por meio do driver SQLite. Isso simplifica o desenvolvimento porque não precisamos configurar um servidor separado, criar um serviço de banco ou estabelecer uma conexão pela rede.



PostgreSQL​


O PostgreSQL é um banco de dados relacional que normalmente funciona como um servidor separado da aplicação. Isso significa que o banco possui seu próprio processo, responsável por receber conexões, executar consultas e gerenciar as informações armazenadas. Uma aplicação Go pode se conectar ao PostgreSQL pela rede, mesmo que o banco esteja executando em outro computador. Podemos representar essa comunicação da seguinte maneira.

       Aplicação Go
|
| Conexão TCP/IP
v
Servidor PostgreSQL
|
v
Banco de dados

O TCP/IP é um conjunto de protocolos que permite a comunicação entre computadores e processos por meio de uma rede. Nesse modelo, a aplicação envia comandos SQL ao servidor PostgreSQL, que processa as operações e retorna os resultados. Essa separação permite que diferentes aplicações compartilhem o mesmo banco de dados.



MySQL​


O MySQL também é um banco de dados relacional que funciona no modelo cliente-servidor. Assim como o PostgreSQL, ele normalmente executa como um serviço separado, recebendo conexões de aplicações que precisam acessar os dados. O funcionamento básico com Go é semelhante.

       Aplicação Go
|
| Conexão TCP/IP
v
Servidor MySQL
|
v
Banco de dados

A aplicação estabelece a comunicação com o servidor MySQL e envia comandos SQL para consultar ou modificar informações.



Diferença entre PostgreSQL e MySQL​


PostgreSQL e MySQL possuem muitas características em comum. Ambos são relacionais, trabalham com SQL, oferecem suporte a transações e permitem que várias aplicações acessem um banco centralizado. O PostgreSQL possui uma arquitetura bastante extensível e oferece recursos avançados para consultas, tipos de dados e operações mais complexas.


O MySQL também atende sistemas grandes e possui recursos avançados, além de ser bastante comum em aplicações web e ambientes que já possuem infraestrutura baseada nele. Não existe uma regra que determine que PostgreSQL seja sempre melhor que MySQL ou o contrário.



database/sql​


O database/sql é um pacote da biblioteca padrão do Go, responsável por fornecer uma interface para trabalhar com bancos de dados relacionais, como SQLite, PostgreSQL e MySQL. Uma interface (nesse contexto) representa um conjunto de funções, métodos e tipos que permitem realizar operações no banco de forma padronizada, sem precisar implementar toda a comunicação diretamente na aplicação.


Isso significa que podemos usar o mesmo pacote para trabalhar com diferentes bancos de dados, mesmo que cada um tenha suas próprias características. O database/sql oferece recursos para abrir conexões, executar comandos SQL, consultar registros, trabalhar com transações e gerenciar conexões com o banco.


Como cada banco de dados possui suas próprias regras e mecanismos de comunicação, o database/sql resolve parte desse desafio, fornecendo uma camada de abstração para executar operações de forma independente do banco de dados utilizado. O PostgreSQL funciona de maneira diferente do SQLite, enquanto o PostgreSQL normalmente depende de um servidor para receber conexões, o SQLite trabalha diretamente com um arquivo de banco de dados.


Sem uma interface padronizada, precisaríamos aprender uma API completamente diferente para cada banco. Por exemplo, independentemente do banco escolhido, podemos trabalhar com métodos como:

  • Open() — cria uma estrutura responsável pelo acesso ao banco.
  • Exec() — executa comandos SQL que não retornam linhas de resultado, como operações de inserção, atualização e remoção.
  • Query() — executa consultas que podem retornar vários registros.
  • QueryRow() — executa consultas em que esperamos apenas um registro.
  • Close() — encerra o acesso ao banco e libera os recursos associados.

Esses métodos fazem parte do database/sql, permitindo manter uma estrutura de código semelhante mesmo quando precisamos trocar de banco. Isso não significa que todos os bancos aceitam exatamente os mesmos comandos SQL. Existem diferenças de sintaxe, tipos de dados e funcionalidades que precisam ser consideradas.


O database/sql não implementa diretamente a comunicação específica de cada banco. Ele depende de um componente chamado driver, que é responsável por realizar essa comunicação. Podemos pensar nessa estrutura assim:

        Aplicação Go
|
v
database/sql
|
v
Driver
|
v
Banco de dados

A aplicação chama os métodos do database/sql, que trabalha em conjunto com o driver para executar as operações no banco. O driver conhece as regras de comunicação do banco escolhido e é responsável por implementar essa parte específica. Dessa forma, o database/sql oferece uma interface comum, enquanto o driver permite que essa interface funcione com determinado banco. Vamos aprofundar essa diferença no próximo tópico.



Exemplo básico​


Imagine que temos uma tabela chamada usuarios, contendo as colunas id e nome. Precisamos criar uma função que receba o identificador de um usuário e retorne seu nome. Com o database/sql, podemos escrever algo assim:

package main

import "database/sql"

// Busca o nome de um usuário pelo ID.
func buscarNome(db *sql.DB, id int) (string, error) {
var nome string

// Executa a consulta e armazena o resultado em nome.
err := db.QueryRow(
"SELECT nome FROM usuarios WHERE id = ?",
id,
).Scan(&nome)

if err != nil {
return "", err
}

return nome, nil
}

Esse exemplo considera que estamos trabalhando com SQLite, que a tabela usuarios já existe e que o banco já foi configurado. A função está isolada apenas para mostrar como o pacote participa de uma consulta. Vamos entender as partes principais.


A linha import "database/sql" importa o pacote responsável por fornecer os recursos de acesso ao banco de dados. Como faz parte da biblioteca padrão do Go, não precisamos instalar esse pacote separadamente.


O db *sql.DB representa a estrutura responsável por gerenciar o acesso ao banco. O tipo sql.DB faz parte do pacote database/sql e será explicado com mais detalhes em um tópico específico.


O db.QueryRow() executa uma consulta SQL em que esperamos receber apenas um registro. Nesse exemplo, estamos buscando o nome de um usuário pelo identificador. O símbolo ? representa um parâmetro da consulta, o valor de id é passado separadamente, evitando a necessidade de montar o comando SQL concatenando valores.


Já .Scan(&nome) é responsável por copiar o valor retornado pela consulta para a variável nome. Por exemplo, se o banco retornar o nome "João", esse valor será armazenado na variável.


O if err != nil verifica se ocorreu algum erro durante a operação. Caso o usuário não seja encontrado, por exemplo, o Scan retorna um erro chamado sql.ErrNoRows, que indica que a consulta não encontrou um registro. Se tudo funcionar corretamente, a função retorna o nome encontrado e um erro igual a nil.



Diferença entre pacote padrão e driver​


O database/sql faz parte da biblioteca padrão do Go e fornece os recursos necessários para executar operações em bancos de dados relacionais. Apesar disso, ele não consegue se comunicar diretamente com SQLite, PostgreSQL ou MySQL, porque cada banco possui suas próprias regras e mecanismos de comunicação.


Para resolver isso, o Go trabalha com dois componentes que possuem responsabilidades diferentes, o pacote database/sql e o driver do banco de dados.


O pacote database/sql fornece tipos e métodos para trabalhar com bancos de dados, como sql.DB, sql.Open(), Exec(), Query() e QueryRow(). A vantagem é que esses recursos seguem uma interface comum, independentemente do banco escolhido. Por exemplo, o método Query() pode ser usado para executar uma consulta tanto no SQLite quanto no PostgreSQL.


O código responsável por chamar esse método continua praticamente igual, embora alguns detalhes da consulta SQL possam mudar. O database/sql também é responsável por gerenciar conexões, controlar o pool de conexões e oferecer mecanismos para trabalhar com transações. Entretanto, ele não possui implementações específicas para se comunicar com cada banco de dados. É justamente essa responsabilidade que fica com o driver.


Um driver é uma biblioteca responsável por permitir que o database/sql se comunique com determinado banco de dados. Cada banco possui características próprias. O PostgreSQL, por exemplo, possui um protocolo de comunicação diferente do MySQL, enquanto o SQLite trabalha diretamente com um banco local, normalmente armazenado em um arquivo. O driver conhece essas características e implementa os mecanismos necessários para executar as operações solicitadas pelo database/sql.


Alguns exemplos de drivers são:

BancoDriver
SQLitemodernc.org/sqlite
PostgreSQLgithub.com/jackc/pgx/v5/stdlib
MySQL/MariaDBgithub.com/go-sql-driver/mysql

Esses drivers são bibliotecas externas, ou seja, não fazem parte da biblioteca padrão do Go e precisam ser adicionados como dependências do projeto. A própria documentação oficial do Go explica que o database/sql não inclui drivers de banco de dados.



Abrindo conexão com o banco​


Para trabalhar com um banco de dados em Go, precisamos configurar o acesso por meio do pacote database/sql e de um driver compatível com o banco escolhido. O método sql.Open() é responsável por criar a estrutura que gerencia esse acesso.


Vamos usar SQLite como exemplo.

package main

import (
"database/sql"
"log"

// Registra o driver SQLite.
_ "modernc.org/sqlite"
)

func main() {
// Configura o acesso ao banco.
db, err := sql.Open("sqlite", "dados.db")
if err != nil {
log.Fatal(err)
}

// Fecha o banco quando a função terminar.
defer db.Close()

// Verifica se o banco está acessível.
if err := db.Ping(); err != nil {
log.Fatal(err)
}
}

O método sql.Open() recebe dois argumentos.

  • "sqlite" representa o nome do driver.
  • "dados.db" representa o caminho do arquivo SQLite.

No caso do SQLite, o arquivo pode ser criado automaticamente quando uma conexão física é aberta, caso ainda não exista.



O sql.Open() não abre necessariamente uma conexão física​


Apesar do nome, sql.Open() não garante que uma conexão com o banco seja estabelecida imediatamente. Ele cria uma estrutura responsável por gerenciar as conexões, que podem ser abertas conforme a aplicação precisar. Por isso, usamos o método Ping() para verificar se o banco está acessível.

// Verifica se conseguimos estabelecer uma conexão.
if err := db.Ping(); err != nil {
log.Fatal(err)
}

Essa verificação é especialmente útil em bancos como PostgreSQL e MySQL, porque o servidor pode estar desligado, inacessível ou com informações de conexão incorretas. O método Ping() tenta estabelecer ou verificar uma conexão e retorna um erro caso a operação falhe.



Entendendo sql.DB​


O tipo sql.DB representa uma estrutura responsável por gerenciar o acesso ao banco de dados. Apesar do nome, sql.DB não representa uma conexão individual, mas um gerenciador de conexões. Quando executamos:

db, err := sql.Open("sqlite", "dados.db")

A variável db recebe um ponteiro para sql.DB, que permite executar consultas, comandos SQL e transações. Podemos observar isso na assinatura do método.

func sql.Open(driverName, dataSourceName string) (*sql.DB, error)

O retorno *sql.DB representa um ponteiro para a estrutura que gerencia o acesso ao banco. Essa estrutura também controla as conexões disponíveis, permitindo que elas sejam reaproveitadas em diferentes operações.



Pool de conexões​


Um pool de conexões é um mecanismo que mantém e gerencia conexões com o banco para que elas possam ser reaproveitadas. Criar uma conexão física pode envolver operações como autenticação e comunicação pela rede, dependendo do banco. Se precisássemos estabelecer uma nova conexão para cada consulta, teríamos um custo desnecessário.


O pool resolve isso mantendo conexões disponíveis para novas operações. O database/sql já possui um pool de conexões integrado, gerenciado pelo sql.DB. Imagine que uma aplicação recebe três solicitações de consulta.

              Aplicação Go
|
v
sql.DB
|
v
Pool de conexões
|
+-------+-------+
| | |
Conn 1 Conn 2 Conn 3
| | |
+-------+-------+
|
v
Banco de dados

O pool pode fornecer uma conexão para cada operação, respeitando os limites configurados. Quando uma consulta termina e libera seus recursos, a conexão pode voltar ao pool e ficar disponível para outra operação. Isso evita criar e encerrar conexões físicas desnecessariamente.


Configurando o pool​


O sql.DB oferece métodos para controlar a quantidade de conexões.

// Limita a quantidade de conexões abertas.
db.SetMaxOpenConns(10)

// Limita a quantidade de conexões ociosas.
db.SetMaxIdleConns(5)

O SetMaxOpenConns(10) define que o gerenciador poderá manter, no máximo, 10 conexões abertas simultaneamente. Se todas estiverem ocupadas, novas operações poderão precisar esperar até que alguma conexão fique disponível.


O SetMaxIdleConns(5) define que até 5 conexões podem permanecer abertas sem estar executando operações, ficando disponíveis para serem reaproveitadas. Também podemos configurar o tempo máximo de vida de uma conexão.

// Define o tempo máximo de vida de uma conexão.
db.SetConnMaxLifetime(30 * time.Minute)

Aqui, uma conexão pode ser reaproveitada durante até 30 minutos de vida. Depois desse período, ela será substituída quando apropriado. Para esse código funcionar, precisamos importar o pacote time.


Como o SQLite trabalha diretamente com um arquivo, precisamos considerar que ele permite apenas uma operação de escrita por vez em cada arquivo de banco. Em projetos simples, podemos limitar o pool a uma conexão.

// Mantém apenas uma conexão aberta.
db.SetMaxOpenConns(1)

// Permite manter essa conexão disponível para reaproveitamento.
db.SetMaxIdleConns(1)

Essa configuração simplifica o controle de acesso ao SQLite e pode reduzir disputas entre operações de escrita. Entretanto, também faz com que as operações compartilhem uma única conexão, limitando a concorrência. Isso não significa que SQLite sempre precise trabalhar com apenas uma conexão. A configuração depende das necessidades da aplicação.



Fechando conexões corretamente​


O sql.DB mantém conexões abertas para que elas possam ser reaproveitadas. Quando a aplicação não precisa mais acessar o banco, podemos chamar Close() para encerrar o gerenciador e liberar os recursos associados.

db, err := sql.Open("sqlite", "dados.db")
if err != nil {
log.Fatal(err)
}

// Fecha o gerenciador quando a função terminar.
defer db.Close()

A palavra defer faz com que a chamada de db.Close() seja executada quando a função atual terminar normalmente ou retornar, mesmo que isso aconteça por causa de um erro tratado com return.


Normalmente, abrimos o sql.DB quando a aplicação inicia e fechamos quando ela é encerrada. Não precisamos chamar db.Close() depois de cada consulta, porque isso impediria que o gerenciador continuasse sendo usado e reaproveitasse suas conexões. O método Close() impede que novas consultas sejam iniciadas por aquele sql.DB e aguarda a conclusão das operações que já começaram.


Existe uma diferença entre fechar o sql.DB e liberar os resultados de uma consulta. Quando usamos Query(), recebemos um *sql.Rows, que representa os registros retornados pelo banco.

rows, err := db.Query("SELECT id, nome FROM usuarios")
if err != nil {
return err
}

// Libera os recursos da consulta ao terminar a função.
defer rows.Close()

O rows.Close() libera os recursos associados ao resultado da consulta, permitindo que a conexão seja reaproveitada pelo pool. Isso não fecha o sql.DB. Ao percorrer todos os registros com rows.Next(), o resultado normalmente é fechado automaticamente, mas chamar defer rows.Close() continua sendo uma boa prática porque também cobre situações em que a leitura termina antes.


A diferença principal é que db.Close() encerra o gerenciador de conexões, enquanto rows.Close() libera os recursos de um resultado de consulta.