Banco de Dados com GoLang
Olá leitor, seja bem vindo a mais uma lição da nossa Jornada de GoLang 😉
Nesta lição, nos iremos mergulhar em um dos assuntos mais importantes para todo desenvolvedor: Como fazer com que nossos dados persistam (fiquem salvos) mesmo quando nossa aplicação for fechada ou reiniciada?
E para respondermos essa pergunta, você precisará entender o que é, e para que serve um banco de dados no mundo da Tecnologia!
Vamos nessa? 🙃
O que é um banco de dados?
Como o próprio nome já nos diz, um banco de dados nada mais é do que um conjunto de dados que estão armazenados dentro de um outro sistema.
Pense em um banco de dados como o HD ou SSD do seu computador, ou quem sabe o cartão de memória do seu celular.
Quando você quer armazenar uma determinada informação de modo que ela não se perca, o ideal é armazenar isso na memória do seu equipamento eletrônico, ou quem sabe armazenar essa informação em nuvem (que por sua vez também salva em outro dispositivo físico rs)
O conceito de banco de dados em tecnologia não é beeeeem um dispositivo feito para armazenamento de qualquer tipo de dado como um HD, SSD ou um cartão de memória, porém com os exemplos anteriores você já vai te dar uma ideia do que esperar 😉
Tecnicamente falando, um banco de dados é um sistema organizado para armazenar, manipular e recuperar dados de forma eficiente. Ele permite que aplicações persistam informações em disco, garantindo durabilidade e integridade mesmo após desligamentos ou falhas na aplicação.
✅ Segue abaixo as principais características de um banco de dados:
Persistência: Armazena dados de forma durável (em disco ou SSD).
Consultas eficientes: Permite buscar informações rapidamente, por meio do uso de linguagens como o SQL
.
Organização estruturada: Usa tabelas, coleções, documentos, índices, etc.
Controle de concorrência: Permite múltiplas transações ao mesmo tempo, mantendo a consistência dos dados.
ACID (para bancos relacionais): Garante Atomicidade, Consistência, Isolamento e Durabilidade nas transações.
Então imagine um banco de dados como se fosse um sistema onde você pode inserir dados que serão armazenados e persistidos, e também pode selecionar tais dados, atualizá-los e até removê-los.
No fundo no fundo, esse sistema de banco de dados, por de baixo dos panos, também está salvando essas informações em um HD, SSD ou até mesmo dentro de um cartão de memória.
A diferença é que ele conta com um gerenciador capaz de manipular tais informações de maneira fácil para quem o utiliza.
Estou te falando isso para você não confundir banco de dados com armazenamento interno, ok?
No mundo da tecnologia existe dois tipos principais de bancos de dados:
O banco de dados relacional (usa a sintaxe SQL) que são criados por meio de estruturas de tabelas, como é o caso dos bancos PostgreSQL
, MySQL
, SQLite
, SQL Server
.
E os bancos de dados não relacionais (NoSQL) que são usados para armazenamento flexível para trabalhar com documentos, chave-valor, grafos, etc.
Exemplo de bancos não relacionais: MongoDB
(documentos), Redis
(chave-valor), Neo4j
(grafo).
Entendendo a estrutura de um banco de dados relacional
Nesta lição, focaremos em aprender a criar um banco de dados relacional usando o MySQL
em conjunto com a linguagem GoLang.
Sendo assim, é de vital importância que você entenda um pouco sobre como funciona a estrutura de uma base de dados relacional 😊
Todo banco de dados relacional é formado por tabelas, que armazenam dados de maneira estruturada, como se fossem planilhas. Cada tabela contém colunas (campos) e linhas (registros).
Tudo começa quando criamos uma base de dados, que pode ser chamada com um nome a sua escolha, onde dentro dessa base você pode ter milhares de tabelas, e dentro dessas tabelas milhares de colunas e linhas, como é demonstrado no exemplo abaixo:

Seu projeto pode ter varias bases de dados, e dentro de cada uma delas podem existir diversas tabelas.
Uma tabela é uma coleção de dados sobre uma entidade específica, como por exemplo, podemos ter uma tabela chamada:
usuarios
: que vai armazenar diversas informações sobre nossos usuários como: identificador único, nome, sobrenome, e-mail, idade etc.produtos
: que vai armazenar as informações dos produtos que serão vendidos como: identificador único, SKU, nome, marca, tipo, preço etc.pedidos
: que vai armazenar todos os pedidos da nossa loja virtual, como: identificar único, identificar do usuário, identificador do produto, quantidade etc.
Já dentro de cada uma de nossas tabelas, temos as colunas que representam os campos que irão armazenar os valores de fato (dados e informações). As colunas definem os tipos de dados que cada registro (linha) da tabela deve conter, por exemplo:
- Um nome (ex: nome, email)
- Um tipo de dado (ex: VARCHAR, INT, DATE)
Ou seja, cada coluna é composta por uma estrutura de chave-valor, onde a chave tem um nome, que vai aceitar um tipo de dado específico.
Por fim, temos os registros que nada mais é do que os valores que serão salvos no banco de dados, por exemplo:
- Micilini Roll,
- 28,
- https://micilini.com,
- etc...
É importante ressaltar que a maioria das tabelas contam com uma chave primária, que identifica de forma única cada registro ali existente, comumente chamado de id
. (Nem toda tabela pode ter uma chave primária)
Além disso, é importante ressaltar que a grande vantagem dos bancos relacionais é que podemos relacionar tabelas entre si, tendo assim sistemas interconectados com outros.
Como esta jornada não é sobre banco de dados, não iremos nos atentar em detalhes mais críticos durante esta lição.
Sendo assim, vamos focar apenas em aprender a como utilizar o MySQL
em conjunto com nossas aplicações feitas com GoLang 😎
Mas antes, você vai precisar instalar o MySQL
na sua máquina local.
Instalando o MySQL na sua máquina local
Como nesta lição nós iremos usar o MySQL
em conjunto com o GoLang, é imprescindível que você faça a instalação dele em sua máquina local.
Vou deixar abaixo alguns links que podem te ajudar:
Já esta com o MySQL
instalado e configurado na sua máquina? Então vamos colocar a mão na massa! 😆
Criando seu projeto de testes
Dentro da pasta JornadaGoLang, nós iremos criar uma nova pasta chamada de 22-banco-de-dados
, onde dentro dela, vamos criar o nosso arquivo main.go
:
package main
func main() {
}
Feito isso, partiu então (só que dessa vez de verdade) aprender a nos comunicar com uma base de dados em conjunto com o GoLang! 😉
Fazendo download do driver do MySQL
Dentro da pasta do seu projeto, vamos precisar baixar um driver que será responsável por se comunicar com o banco de dados MySQL
que está instalado na sua máquina local.
Sendo assim, com o terminal aberto dentro da pasta do projeto, execute os seguintes comandos abaixo:
go mod init 22-banco-de-dados
go get -u github.com/go-sql-driver/mysql
Com os comandos acima, nós estamos inicializando nosso repositório, e baixando o driver do MySQL
.
Sem este driver, é impossível o GoLang se comunicar com o MySQL
, ok?
Importando as bibliotecas no seu projeto
Já dentro do seu main.go
, a primeira coisa que precisamos fazer é importar as bibliotecas que serão usadas durante o nosso projeto.
Bibliotecas estas que são necessárias para abrirmos uma conexão com o banco de dados (MySQL) e realizar nossas operações do tipo CRUD (Create, Read, Update e Delete):
package main
import(
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
"database/sql"
: Essa é uma biblioteca padrão do Go que fornece uma interface genérica para interação com bancos de dados SQL
(como MySQL, PostgreSQL, SQLite, etc.).
Como ela não sabe falar com o banco de dados sozinha, ela precisa de um driver, que é a nossa próxima importação...
_ "github.com/go-sql-driver/mysql"
: Esse driver registra-se dentro do database/sql
como um driver chamado "mysql"
, quando é importado.
É importante observar que quando importamos essa biblioteca com _
(underscore), significa que estamos importando só para executar o init()
do pacote, mas não usá-lo diretamente.
Feito isso, nós temos todos os componentes necessário para brincarmos com o banco MySQL
🙂
Criando nossa tabela de testes
Neste primeiro momento, dentro do seu terminal, vamos criar uma nova tabela usando o MySQL
, para isso, vamos digitar o seguinte comando para entrarmos na nossa base de dados:
mysql -u root -p
Após entrar com a sua senha, você verá no terminal uma chamada de boas vindas, e um espaço exclusivo para inserirmos códigos SQL
:

Ali dentro, vamos criar um novo banco chamado de jornadagolang
, para isso execute o seguinte comando dentro do terminal do MySQL
:
CREATE DATABASE jornadagolang;
Depois, selecione o banco de dados que acabamos de criar:
USE jornadagolang;
Se tudo der certo, uma nova base de dados chamada jornadagolang
acaba de ser criada, e com isso já estamos prontos para fazermos nossa primeira conexão 😍
Por hora, você pode fechar o terminal da sua máquina, e trabalhar diretamente com o seu editor de códigos.
Minha primeira conexão com o MySQL
Antes de mais nada, é crucial que você tenha em mãos (ou pelo menos se lembre) o seu login e senha que criou durante a configuração do MySQL.
Está com ele em mãos? Então partiu atualizar o nosso main.go
:
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/jornadagolang")
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal("Erro ao conectar:", err)
}
log.Println("Conexão OK!")
}
Vamos ás explicações 😋
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/jornadagolang")
sql.Open(...)
: Cria uma conexão lógica (um pool de conexões) com o banco de dados.
"mysql"
: Indica que vamos usar o driver que foi registrado (via import com _ "github.com/go-sql-driver/mysql").
"user:password@tcp(127.0.0.1:3306)/jornadagolang"
: contém uma string
contendo todas as informações necessárias para abrir uma conexão com o banco de dados.
user
: nome do usuário do banco (ex: root),password
: senha do usuário,tcp(127.0.0.1:3306)
: conexão local, na porta padrão do MySQL, onde costuma ser esse valor mesmo,/jornadagolang
: nome do banco de dados que iremos usar.
É importante ressaltar que sql.Open
não testa a conexão de verdade ainda, só prepara o objeto.
if err != nil {
log.Fatal(err)
}
O comando acima verifica se houve algum tipo de erro durante a tentativa de abertura de conexão. Caso houver, o programa é encerrado com log.Fatal
, que também imprime o erro no terminal.
defer db.Close()
O comando acima garante que a conexão com o banco será fechada automaticamente ao final da função main
.
É como se você falasse para o GoLang assim: "execute isso por último, quando a função terminar.".
err = db.Ping()
Neste momento em específico, nós estamos de fato testando a conexão com o banco de dados.
O comando db.Ping()
tenta enviar um "ping" ao servidor passando as credenciais que definimos dentro do sql.Open
.
if err != nil {
log.Fatal("Erro ao conectar:", err)
}
Se o Ping()
falhar (ex: senha errada, banco fora do ar...), mostra o erro e encerra o programa.
log.Println("Conexão OK!")
Por fim, se der tudo certo, veremos uma mensagem de OK, indicando que a comunicação com a base de dados foi um sucesso.
E aí está, você acabou de fazer a sua primeira conexão com o banco de dados MySQL
via GoLang 🥳
Entretanto, você pode ter algumas dúvidas!
Dúvida: E se a minha senha conter um @ no final?
Nesse caso, o melhor a ser feito é repetir o @ que vem logo após a sua senha. Supondo que a sua senha seja 123456@, suas credenciais deverão ser:
db, err := sql.Open("mysql", "root:123456@@tcp(127.0.0.1:3306)/jornadagolang")
Note que ficou 123456@@/
. Sendo o último @
a indicação da porta TCP.
Dúvida: Sempre precisamos passar o nome do banco de dados ou o tcp dentro das credenciais?
Não, isto não é 100% necessário, vejamos como fazer isso de forma mais simples apenas passando o usuário e a senha:
db, err := sql.Open("mysql", "root:123456@@/")
Entretanto, você precisa selecionar um banco de dados com o Exec
posteriormente (veremos como isso funciona no próximo tópico).
Dúvida: E se o meu banco de dados MySQL estiver na nuvem? Tem algum jeito de se comunicar com ele pelo meu projeto local?
Sim, você ainda pode se comunicar com um banco de dados MySQL
que está na nuvem usando seu projeto local em Go — desde que o banco esteja acessível publicamente ou via VPN/túnel seguro.
Veja como isso pode ser feito:
db, err := sql.Open("mysql", "admin:senhasecreta@tcp(db.cloudhost.com:3306)/jornadagolang")
Usando o comando Exec
Quando nós queremos executar uma operação, seja ela:
INSERT
UPDATE
DELETE
CREATE TABLE
TRUNCATE
DROP
ALTER
- etc
Você vai precisar fazer o uso do comando Exec
!
Ele é responsável por executar comandos SQL
(que não retornam resultados) dentro das suas tabelas do banco de dados.
A sintaxe básica do Exec
é a seguinte:
res, err := db.Exec("COMANDO SQL", argumentos...)
res
: do tipo sql.Result
, permite saber quantas linhas foram alteradas, e o último ID inserido (LastInsertId
).
err
: retorna um erro caso algo dê errado (ex: SQL malformado, falha de conexão, etc.)
Por exemplo, supondo que temos uma tabela chamada usuarios
dentro da base jornadagolang
, podemos usar a seguinte lógica para criar novos registros.
nome := "Micilini"
email := "hey@micilini.com"
res, err := db.Exec("INSERT INTO usuarios (nome, email) VALUES (?, ?)", nome, email)
if err != nil {
log.Fatal("Erro ao inserir:", err)
}
id, _ := res.LastInsertId()
fmt.Println("Usuário inserido com ID:", id)
Os ?
são placeholders de parâmetros para evitar SQL Injection.
res.LastInsertId()
é responsável por pega o ID da última linha inserida (se for AUTO_INCREMENT).
res.RowsAffected()
retorna a quantidade de linhas que foram afetadas (útil em UPDATE/DELETE).
O que é SQL Injection e por que devemos nos proteger dele?
SQL Injection é uma vulnerabilidade de segurança crítica que permite que um usuário mal-intencionado injete comandos SQL maliciosos em campos de entrada (como formulários) para manipular ou acessar indevidamente o banco de dados.
Mas daí você se pergunta: usuário injetando comandos SQL? Mas eu sou o desenvolvedor, não tem nenhum usuário inserindo dados para mim!
No momento! Pois futuramente, creio que você vá construir aplicações em GoLang e tenha que lidar com os dados inseridos pelo usuário no seu sistema.
E quando isso chegar, é importante que você saiba lidar com o SQL Injection.
O que um atacante pode fazer com SQL Injection?
- 📂 Ler dados confidenciais (ex: senhas, e-mails, cartões)
- 🧨 Deletar tabelas ou bancos de dados inteiros
- 🛠️ Modificar dados
- 👤 Fazer login sem senha (bypass de autenticação)
- 📊 Ver estrutura do banco de dados
No tópico anterior, você viu como lidar com o SQL Inection passando o ?
durante um insert
:
res, err := db.Exec("INSERT INTO usuarios (nome, email) VALUES (?, ?)", nome, email)
Mas caso desejar, você pode querer ser pego desprevenido e optar por não se proteger (NÃO FAÇA ISSO):
// PERIGOSO: vulnerável a SQL Injection
query := "INSERT INTO usuarios (nome, email) VALUES ('" + nome + "', '" + email + "')"
res, err := db.Exec(query)
Criando uma função para encapsular o Exec
Como nos próximos tópicos nós iremos fazer o uso do comando Exec
por diversas vezes, que tal criar uma função encapsuladora que seja responsável por receber diversas querys do SQL
e executar de um único ponto?
func ExecQuery(db *sql.DB, query string, args ...interface{}) (sql.Result, error) {
result, err := db.Exec(query, args...)
if err != nil {
return nil, fmt.Errorf("erro ao executar query: %w", err)
}
return result, nil
}
Com essa função, não precisamos ficar repetindo o comando Exec
toda vez que formos fazer uma operação na nossa base de dados (você vai me agradecer por isso no futuro, repetição de código não é legal).
Caso tivéssemos uma tabela chamada usuarios
, poderíamos fazer o uso da função ExecQuery
da seguinte forma:
res, err := ExecQuery(db, "INSERT INTO usuarios (nome, email) VALUES (?, ?)", "William", "william@example.com")
if err != nil {
log.Fatal(err)
}
id, _ := res.LastInsertId()
fmt.Println("ID inserido:", id)
Com ela nós podemos:
- Tornar o código mais limpo
- Evitar duplicação de tratamento de erros
- Facilitar testes e logs
- E principalmente: manter a segurança contra o SQL Injection
Agora que você já sabe como isso tudo funciona, vamos seguir o nosso script e aprender a criar uma tabela diretamente pelo GoLang 😌
Criando sua primeira tabela via Go
Dentro do arquivo do seu projeto, vamos unir algumas coisas que já foram feitas anteriormente, e adicionar a lógica responsável por criar nossa tabela de usuários
:
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"log"
"fmt"
)
func ExecQuery(db *sql.DB, query string, args ...interface{}) (sql.Result, error) {
result, err := db.Exec(query, args...)
if err != nil {
return nil, fmt.Errorf("erro ao executar query: %w", err)
}
return result, nil
}
func main() {
db, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/jornadagolang")
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal("Erro ao conectar:", err)
}
log.Println("Conexão OK!")
// Criando sua primeira tabela de usuarios
query := `
CREATE TABLE IF NOT EXISTS usuarios (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100),
email VARCHAR(100),
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
_, err = ExecQuery(db, query)
if err != nil {
log.Fatal("Erro ao criar tabela:", err)
}
log.Println("Tabela 'usuarios' criada com sucesso!")
}
Não há nada de novo com o código acima, com exceção desse bloco aqui:
query := `
CREATE TABLE IF NOT EXISTS usuarios (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(100),
email VARCHAR(100),
criado_em TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
_, err = ExecQuery(db, query)
if err != nil {
log.Fatal("Erro ao criar tabela:", err)
}
log.Println("Tabela 'usuarios' criada com sucesso!")
No código acima criamos uma variável chamada query
que contém toda a string
necessária para criar uma nova tabela chamada usuarios
caso ela não exista.
Em seguida, chamamos a nossa mais nova função encapsuladora (ExecQuery
)
Por fim, se tudo der certo você receberá uma mensagem de sucesso, alegando que a tabela foi criada com sucesso 🥳
Tabela 'usuarios' criada com sucesso!
Se você estiver usando a ferramenta MySQL WorkBench, verá que uma nova tabela acaba de ser criada com sucesso:

Legal, não acha?
Dúvida: será que é possível criar uma base de dados por meio do comando Exec()?
Sim isso é totalmente possível, observe outro exemplo abaixo:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql"
)
func ExecQuery(db *sql.DB, query string, args ...interface{}) (sql.Result, error) {
result, err := db.Exec(query, args...)
if err != nil {
return nil, fmt.Errorf("erro ao executar query: %w", err)
}
return result, nil
}
func main() {
// Conecta sem banco específico para criar o banco
dbRoot, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/")
if err != nil {
log.Fatal(err)
}
defer dbRoot.Close()
_, err = ExecQuery(dbRoot, "CREATE DATABASE IF NOT EXISTS jornadagolang")
if err != nil {
log.Fatal(err)
}
log.Println("Banco de dados criado/verificado.")
// Agora conecta no banco jornadagolang
db, err := sql.Open("mysql", "root:123123@tcp(127.0.0.1:3306)/jornadagolang")
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = ExecQuery(db, "DROP TABLE IF EXISTS users")
if err != nil {
log.Fatal(err)
}
_, err = ExecQuery(db, `
CREATE TABLE users (
id INT AUTO_INCREMENT,
name VARCHAR(120),
email VARCHAR(255),
password VARCHAR(60),
PRIMARY KEY (id)
)
`)
if err != nil {
log.Fatal(err)
}
log.Println("Tabela users criada com sucesso!")
}
Inserindo dados na sua tabela
Agora que já temos uma tabela, que tal aprendermos a inserir alguns dados nela?
O comando que iremos usar agora é o INSERT
do SQL
e ele é usado para inserir valores na tabela:
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"log"
"fmt"
)
func ExecQuery(db *sql.DB, query string, args ...interface{}) (sql.Result, error) {
result, err := db.Exec(query, args...)
if err != nil {
return nil, fmt.Errorf("erro ao executar query: %w", err)
}
return result, nil
}
func main() {
db, err := sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/jornadagolang")
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Ping()
if err != nil {
log.Fatal("Erro ao conectar:", err)
}
log.Println("Conexão OK!")
// Fazendo um INSERT de usuário
nome := "Micilini Roll"
email := "hey@micilini.com"
insertQuery := "INSERT INTO usuarios (nome, email) VALUES (?, ?)"
res, err := ExecQuery(db, insertQuery, nome, email)
if err != nil {
log.Fatal("Erro ao inserir usuário:", err)
}
id, _ := res.LastInsertId()
log.Printf("Usuário inserido com sucesso! ID: %d\n", id)
}
O comando responsável por fazer o INSERT
é este:
insertQuery := "INSERT INTO usuarios (nome, email) VALUES (?, ?)"
res, err := ExecQuery(db, insertQuery, nome, email)
if err != nil {
log.Fatal("Erro ao inserir usuário:", err)
}
Note que fizemos o uso da função ExecQuery
de forma segura contra SQL Injection.
Além disso, usamos o comando id, _ := res.LastInsertId()
para recuperar o último id que foi inserido no banco de dados.
De resto, são apenas lógicas aprendidas em lições anteriores desta jornada, além de querys da linguagem SQL
😉
Recuperando a listagem de dados na sua tabela
Sempre após um INSERT
, é sempre bom validarmos se valores que foram inseridos em nossas tabelas realmente estão lá.
Não só isso, como um sistema em GoLang pode, e deve querer consultar registros que foram feitos anteriormente.
No caso do SQL
, nós usamos o SELECT
para recuperar a listagem de dados que nós queremos, e há diferentes formas de fazer isso.
1) SELECT geral de todos os usuários:
rows, err := db.Query("SELECT id, nome, email, criado_em FROM usuarios")
if err != nil {
log.Fatal("Erro ao buscar usuários:", err)
}
defer rows.Close()
for rows.Next() {
var id int
var nome, email string
var criadoEm string
err := rows.Scan(&id, &nome, &email, &criadoEm)
if err != nil {
log.Println("Erro ao ler linha:", err)
}
fmt.Printf("ID: %d | Nome: %s | Email: %s | Criado em: %s\n", id, nome, email, criadoEm)
}
2) SELECT com WHERE (parâmetro seguro):
emailFiltro := "hey@micilini.com"
row := db.QueryRow("SELECT id, nome FROM usuarios WHERE email = ?", emailFiltro)
var id int
var nome string
err = row.Scan(&id, &nome)
if err != nil {
log.Println("Usuário não encontrado:", err)
} else {
fmt.Printf("Usuário encontrado: ID %d, Nome %s\n", id, nome)
}
3) SELECT com ORDER BY e LIMIT:
rows, err := db.Query("SELECT nome, email FROM usuarios ORDER BY criado_em DESC LIMIT 5")
if err != nil {
log.Fatal("Erro ao buscar últimos usuários:", err)
}
defer rows.Close()
for rows.Next() {
var nome, email string
rows.Scan(&nome, &email)
fmt.Printf("Nome: %s | Email: %s\n", nome, email)
}
4) SELECT com agregação (COUNT, SUM etc):
row = db.QueryRow("SELECT COUNT(*) FROM usuarios")
var total int
err = row.Scan(&total)
fmt.Println("Total de usuários:", total)
5) SELECT com JOIN entre duas tabelas:
Supondo que tenhamos uma tabela chamada perfis
:
CREATE TABLE perfis (
id INT AUTO_INCREMENT PRIMARY KEY,
usuario_id INT,
descricao VARCHAR(255),
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
);
Podemos usar o código abaixo para fazer o JOIN
:
query := `
SELECT u.nome, u.email, p.descricao
FROM usuarios u
JOIN perfis p ON u.id = p.usuario_id
WHERE u.id = ?`
row := db.QueryRow(query, 1)
var nome, email, descricao string
err = row.Scan(&nome, &email, &descricao)
fmt.Printf("Nome: %s | Email: %s | Perfil: %s\n", nome, email, descricao)
6) SELECT com LIKE:
filtro := "%micilini%"
rows, err = db.Query("SELECT nome FROM usuarios WHERE nome LIKE ?", filtro)
defer rows.Close()
for rows.Next() {
var nome string
rows.Scan(&nome)
fmt.Println("Nome parecido:", nome)
}
Geralmente dentro de alguns sistemas, você vai encontrar muitas funções que podem encapsular algumas chamadas específicas de banco de dados, como é o caso das funções SelectAllUsuarios
e GetUsuarioByID
:
func SelectAllUsuarios(db *sql.DB) ([]map[string]interface{}, error) {
rows, err := db.Query("SELECT id, nome, email, criado_em FROM usuarios")
if err != nil {
return nil, fmt.Errorf("erro ao buscar usuários: %w", err)
}
defer rows.Close()
var usuarios []map[string]interface{}
for rows.Next() {
var id int
var nome, email string
var criadoEm string
if err := rows.Scan(&id, &nome, &email, &criadoEm); err != nil {
return nil, err
}
usuarios = append(usuarios, map[string]interface{}{
"id": id,
"nome": nome,
"email": email,
"criado_em": criadoEm,
})
}
return usuarios, nil
}
func GetUsuarioByID(db *sql.DB, id int) (map[string]interface{}, error) {
row := db.QueryRow("SELECT nome, email, criado_em FROM usuarios WHERE id = ?", id)
var nome, email, criadoEm string
if err := row.Scan(&nome, &email, &criadoEm); err != nil {
if err == sql.ErrNoRows {
return nil, nil // Não encontrado, sem erro
}
return nil, fmt.Errorf("erro ao buscar usuário: %w", err)
}
return map[string]interface{}{
"id": id,
"nome": nome,
"email": email,
"criado_em": criadoEm,
}, nil
}
Considerando ambas funções acima, você pode fazer o uso de cada uma delas dentro da sua main
da seguinte forma:
usuarios, err := SelectAllUsuarios(db)
if err != nil {
log.Fatal(err)
}
for _, u := range usuarios {
fmt.Printf("ID: %v | Nome: %v | Email: %v | Criado: %v\n", u["id"], u["nome"], u["email"], u["criado_em"])
}
usuario, err := GetUsuarioByID(db, 1)
if err != nil {
log.Fatal(err)
}
if usuario != nil {
fmt.Printf("Usuário #1: %+v\n", usuario)
} else {
fmt.Println("Usuário com ID 1 não encontrado.")
}
Dúvida: Por que não usei a ExecQuery nos meus SELECTs?
Ela não foi usada, pois a ExecQuery
faz o uso do comando db.Exec
, que serve apenas para comandos que não retornam resultados (insert, update, delete etc).
Sendo assim, podemos dizer que o comando Exec()
não nos serve para a realização dos nossos SELECTs
, por isso temos que fazer o uso de dois comandos:
db.Query(...)
→ para vários resultados (rows
).db.QueryRow(...)
→ para um único resultado (row
).
Atualizando dados da tabela
Para atualizar os registros de uma determinada tabela do nosso banco, nós fazemos o uso do comando UPDATE
.
Neste caso, diferente do SELECT
, aqui nós podemos fazer o uso da nossa ExecQuery
, pois estamos enviando valores para o banco.
Mas antes disso, dê uma olhada na sintaxe básica de um UPDATE
:
query := "UPDATE usuarios SET campo1 = ?, campo2 = ? WHERE id = ?"
_, err := ExecQuery(db, query, valor1, valor2, id)
Existem varias formas diferentes de executarmos um UPDATE
, vejamos:
1) Atualizando o nome de um usuário por ID:
nomeNovo := "Roll Atualizado"
id := 1
_, err := ExecQuery(db, "UPDATE usuarios SET nome = ? WHERE id = ?", nomeNovo, id)
if err != nil {
log.Fatal("Erro ao atualizar nome:", err)
}
fmt.Println("Nome atualizado com sucesso!")
2) Atualizando nome e o e-mail ao mesmo tempo:
nomeNovo := "Maria Silva"
emailNovo := "maria@micilini.com"
id := 2
_, err := ExecQuery(db, "UPDATE usuarios SET nome = ?, email = ? WHERE id = ?", nomeNovo, emailNovo, id)
3) Atualizando todos os usuários com e-mail @micilini.com para @empresa.com:
_, err := ExecQuery(db, "UPDATE usuarios SET email = REPLACE(email, '@micilini.com', '@empresa.com') WHERE email LIKE ?", "%@micilini.com")
4) Limpando nomes nulos (substituir por "Usuário Anônimo"):
_, err := ExecQuery(db, "UPDATE usuarios SET nome = ? WHERE nome IS NULL OR nome = ''", "Usuário Anônimo")
5) Atualizando valores por intervalos de ID:
_, err := ExecQuery(db, "UPDATE usuarios SET nome = ? WHERE id BETWEEN ? AND ?", "Atualizado em massa", 10, 20)
6) Atualizando por data de criação:
_, err := ExecQuery(db, "UPDATE usuarios SET nome = ? WHERE criado_em < NOW() - INTERVAL 7 DAY", "Inativo")
7) Atualizando com base em uma subquery (mais avançado):
CREATE TABLE perfis (
id INT AUTO_INCREMENT,
nome_padrao VARCHAR(100)
);
INSERT INTO perfis (id, nome_padrao) VALUES (1, 'Novo Nome Padrão');
_, err := ExecQuery(db, `
UPDATE usuarios
SET nome = (SELECT nome_padrao FROM perfis WHERE id = 1)
WHERE id = ?`, 1)
8) Validando quantas linhas foram afetadas após uma atualização:
res, err := ExecQuery(db, "UPDATE usuarios SET nome = ? WHERE id = ?", "Atualizado", 1)
if err != nil {
log.Fatal(err)
}
rows, _ := res.RowsAffected()
fmt.Printf("Linhas atualizadas: %d\n", rows)
Em alguns sistemas feitos com GoLang, você vai se deparar com funções prontas que executam uma determinada lógica como é o caso do AtualizarUsuario()
:
func AtualizarUsuario(db *sql.DB, id int, nome, email string) (int64, error) {
if id <= 0 {
return 0, fmt.Errorf("ID inválido")
}
query := "UPDATE usuarios SET"
args := []interface{}{}
updates := []string{}
if nome != "" {
updates = append(updates, " nome = ?")
args = append(args, nome)
}
if email != "" {
updates = append(updates, " email = ?")
args = append(args, email)
}
if len(updates) == 0 {
return 0, fmt.Errorf("Nenhum campo fornecido para atualização")
}
query += strings.Join(updates, ",") + " WHERE id = ?"
args = append(args, id)
res, err := ExecQuery(db, query, args...)
if err != nil {
return 0, err
}
rows, err := res.RowsAffected()
if err != nil {
return 0, fmt.Errorf("erro ao obter linhas afetadas: %w", err)
}
return rows, nil
}
Essa função poderia ser usada da seguinte forma dentro da sua main()
:
linhas, err := AtualizarUsuario(db, 1, "Novo Nome", "")
if err != nil {
log.Fatal("Erro ao atualizar:", err)
}
fmt.Printf("Linhas alteradas: %d\n", linhas)
Observação: tome muito cuidado ao executar um UPDATE
sem o where
, pois ele pode atualizar toda a sua base de dados.
Removendo dados da tabela
Por fim, temos o famoso DELETE
do SQL
que é usado para remover nossos registros da tabela.
Sua sintaxe é:
"DELETE FROM usuarios WHERE id = ?"
Vejamos agora alguns exemplos do seu uso:
1) Deletar usuário por ID:
id := 3
_, err := ExecQuery(db, "DELETE FROM usuarios WHERE id = ?", id)
if err != nil {
log.Fatal("Erro ao deletar usuário:", err)
}
fmt.Println("Usuário deletado com sucesso!")
2) Deletar por e-mail exato:
email := "william@example.com"
_, err := ExecQuery(db, "DELETE FROM usuarios WHERE email = ?", email)
if err != nil {
log.Fatal("Erro ao deletar por e-mail:", err)
}
3) Deletar todos os usuários com domínio @micilini.com:
_, err := ExecQuery(db, "DELETE FROM usuarios WHERE email LIKE ?", "%@micilini.com")
if err != nil {
log.Fatal("Erro ao deletar usuários com email da micilini:", err)
}
4) Deletar usuários criados há mais de 30 dias:
_, err := ExecQuery(db, "DELETE FROM usuarios WHERE criado_em < NOW() - INTERVAL 30 DAY")
if err != nil {
log.Fatal("Erro ao deletar usuários antigos:", err)
5) Deletar todos os usuários com nome nulo ou em branco:
_, err := ExecQuery(db, "DELETE FROM usuarios WHERE nome IS NULL OR nome = ''")
if err != nil {
log.Fatal("Erro ao deletar nomes vazios:", err)
}
6) Deletar múltiplos usuários por ID (em lote):
ids := []int{2, 4, 6}
placeholders := strings.TrimRight(strings.Repeat("?,", len(ids)), ",")
query := fmt.Sprintf("DELETE FROM usuarios WHERE id IN (%s)", placeholders)
args := make([]interface{}, len(ids))
for i, v := range ids {
args[i] = v
}
_, err := ExecQuery(db, query, args...)
if err != nil {
log.Fatal("Erro ao deletar múltiplos usuários:", err)
}
Para saber quantos registros foram deletados após um DELETE
, você pode usar a seguinte lógica abaixo:
res, err := ExecQuery(db, "DELETE FROM usuarios WHERE email = ?", "teste@exemplo.com")
if err != nil {
log.Fatal(err)
}
linhas, _ := res.RowsAffected()
fmt.Printf("Linhas deletadas: %d\n", linhas)
Observação: tome muito cuidado ao executar um DELETE
sem o where
, pois ele pode apagar toda a sua base de dados.
Realizando operações no banco por meio de uma struct
Anteriormente nós vimos como realizar operações no banco de dados retornando um map[string]interface{}
.
Mas você sabia que é possível fazer isso por meio de uma struct
?
Considerando que temos uma tabela chamada usuarios
e queremos retornar alguns de seus dados, podemos fazer isso da seguinte forma:
type Usuario struct {
ID int
Nome string
Email string
CriadoEm time.Time
}
func GetUsuarioByID(db *sql.DB, id int) (*Usuario, error) {
row := db.QueryRow("SELECT id, nome, email, criado_em FROM usuarios WHERE id = ?", id)
var u Usuario
err := row.Scan(&u.ID, &u.Nome, &u.Email, &u.CriadoEm)
if err != nil {
return nil, err
}
return &u, nil
}
O uso de uma struct
é uma boa alternativa para manter nossos dados seguros e mais organizados.
Uso de Transactions
Quando você tem uma situação na qual múltiplas operações precisam acontecer simultaneamente, o uso de transactions
se torna uma boa escolha.
Vejamos um exemplo:
tx, err := db.Begin()
if err != nil {
log.Fatal(err)
}
_, err = tx.Exec("INSERT INTO usuarios (nome, email) VALUES (?, ?)", "João", "joao@ex.com")
if err != nil {
tx.Rollback()
log.Fatal("Erro ao inserir:", err)
}
_, err = tx.Exec("DELETE FROM perfis WHERE usuario_id = ?", 5)
if err != nil {
tx.Rollback()
log.Fatal("Erro ao deletar perfil:", err)
}
err = tx.Commit()
if err != nil {
log.Fatal("Erro ao confirmar transação:", err)
}
Paginação com LIMIT + OFFSET
Em alguns sistemas online, trabalhamos com paginação, que envolve recuperar registros de uma determinada tabela pedaço por pedaço.
Saber como realizar uma paginação pode te ajudar na construção de APIs, dashboards e interfaces com muitos dados. Vejamos um exemplo:
func ListarUsuariosPaginado(db *sql.DB, page, pageSize int) ([]Usuario, error) {
offset := (page - 1) * pageSize
rows, err := db.Query("SELECT id, nome, email, criado_em FROM usuarios ORDER BY id LIMIT ? OFFSET ?", pageSize, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var usuarios []Usuario
for rows.Next() {
var u Usuario
rows.Scan(&u.ID, &u.Nome, &u.Email, &u.CriadoEm)
usuarios = append(usuarios, u)
}
return usuarios, nil
}
Validação de Entrada / Sanitização
Antes de inserir qualquer tipo de valor na sua base de dados, é crucial que você valide os dados de forma a garantir que eles se encaixam perfeitamente nas suas tabelas, e que não vão gerar erros ou problemas na sua aplicação.
E isso pode ser feito simplesmente por meio de uma estrutura de controle como um simples if
:
func ValidarUsuario(nome, email string) error {
if strings.TrimSpace(nome) == "" {
return fmt.Errorf("Nome é obrigatório")
}
if !strings.Contains(email, "@") {
return fmt.Errorf("Email inválido")
}
return nil
}
Observação: nunca confie 100% nos dados que o seu usuário irá trazer. Ou em dados que o seu front-end vai trazer para o back-end da sua aplicação.
Para validações mais robustas, eu recomendo o uso da biblioteca go-playground/validator.
Separando camadas (models, services, dbutils)
Quando falamos de projetos gigantescos, é sempre bom falar também de arquitetura de software.
Manter uma arquitetura limpa pode ajudar o seu time a entender o seu código, e manter a manutenção futura do mesmo.
Pensando nisso, deixo abaixo uma dica de como você pode adotar para estruturar os arquivos e pastas do seu projeto, deixando-o mais organizado:
/cmd
main.go
/internal
/models
usuario.go // structs e métodos de banco
/services
usuario_service.go // lógica de negócio
/dbutils
database.go // ExecQuery, QueryRow, BeginTx, etc.
Cada camada ali existente tem uma responsabilidade única, por exemplo:
models/
: aqui estão todos os arquivos que lidam com banco estructs
.services/
: aqui estão todos os arquivos que lidam com as regras de negócio.dbutils/
: aqui estão todas as abstrações de conexão com seu banco de dados.
Vejamos um exemplo do database.go
:
package dbutils
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func ConectarBanco(dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("erro ao abrir conexão: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("erro ao testar conexão: %w", err)
}
return db, nil
}
func ExecQuery(db *sql.DB, query string, args ...interface{}) (sql.Result, error) {
result, err := db.Exec(query, args...)
if err != nil {
return nil, fmt.Errorf("erro ao executar query: %w", err)
}
return result, nil
}
func QueryRows(db *sql.DB, query string, args ...interface{}) (*sql.Rows, error) {
rows, err := db.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("erro ao buscar linhas: %w", err)
}
return rows, nil
}
func QueryRow(db *sql.DB, query string, args ...interface{}) *sql.Row {
return db.QueryRow(query, args...)
}
func BeginTx(db *sql.DB) (*sql.Tx, error) {
tx, err := db.Begin()
if err != nil {
return nil, fmt.Errorf("erro ao iniciar transação: %w", err)
}
return tx, nil
}
Em alguns momentos você verá o database.go
sendo chamado por databaseHelper
(ou db helper), que nada mais é do que um nome informal para funções utilitárias de acesso ao banco de dados, que contam com diversas funções genéricas que realizam nossas operações de CRUD.
Logging Estruturado
O logging estruturado é uma prática cada vez mais essencial em sistemas modernos — especialmente em APIs, microserviços e backends que crescem com o tempo.
Ele é uma prática de gravar logs em formato de dados legível por máquina, como JSON
, em vez de simples string
.
Vejamos um exemplo de um log não estruturado (método tradicional):
[INFO] Usuário criado: ID=12, Nome=Micilini, Email=micilini@ex.com
Agora vejamos um exemplo de um log estruturado com JSON
:
{
"level": "info",
"timestamp": "2025-05-17T19:41:22Z",
"event": "usuario_criado",
"usuario_id": 12,
"nome": "Micilini",
"email": "micilini@ex.com"
}
Por que eu devo fazer o uso de logs estruturados? 🤔
Melhor análise e rastreabilidade: ferramentas como ElasticSearch, Grafana Loki, Datadog, AWS CloudWatch ou Google Cloud Logging conseguem interpretar JSON
automaticamente.
Facilidade de debug e monitoramento: isso inclui filtrar todos os logs do usuário por um determinado parâmetro, buscar erros de autenticação e monitorar transações lentas.
Padrão em ambientes corporativos/produtivos: o uso de logs estruturados já é adotado por grandes corporações e projetos, tornando-se de vital importância para o seu aprendizado.
Vejamos um exemplo de como criar um log não estruturado usando o log.Printf()
:
log.Printf("[INFO] Novo usuário criado: %s <%s>", nome, email)
Vejamos agora um exemplo de como criar um log estruturado usando a biblioteca logrus
:
import log "github.com/sirupsen/logrus"
log.WithFields(log.Fields{
"usuario_id": id,
"email": email,
}).Info("Usuário cadastrado com sucesso")
Repositório da lição
Todos os arquivos relacionados com esta lição, podem ser encontrados nos seguintes repositórios abaixo:
Conclusão
Nesta lição, você aprendeu a trabalhar com a base de dados MySQL em conjunto com GoLang.
Até a próxima 😀