Maps
O Map é uma estrutura usada para guardar valores associados a uma chave. A ideia é parecida com um dicionário em Python ou um objeto simples em JavaScript, mas em Go o tipo da chave e o tipo do valor precisam estar definidos.
idades := map[string]int{
"Ana": 20,
"Carlos": 25,
}
Aqui, criamos um map onde a chave é string e o valor é int.
chave valor
Ana -> 20
Carlos -> 25
Use map[string]any quando os dados são realmente dinâmicos, por exemplo:
- JSON genérico
- configurações variadas
- dados vindos de API externa
- estrutura temporária para testes
Por que maps existem
Os Slices são bons quando queremos guardar uma lista de valores em ordem.
nomes := []string{"Ana", "Carlos", "Bia"}
Mas às vezes queremos acessar um valor por uma chave, não por uma posição. Por exemplo, em vez de pegar a idade pela posição 0, 1 ou 2, queremos pegar pela chave "Ana".
idades["Ana"]
Isso deixa o código mais direto quando existe uma relação entre uma chave e um valor.
Criando um map com valores iniciais
package main
import "fmt"
func main() {
// Cria um map onde a chave é string
// e o valor é int.
idades := map[string]int{
"Ana": 20,
"Carlos": 25,
"Bia": 18,
}
fmt.Println(idades)
}
Resultado possível:
map[Ana:20 Bia:18 Carlos:25]
A ordem pode aparecer diferente, mas os Maps em Go não garantem ordem de iteração ou impressão.
Acessando um valor pela chave
Para acessar um valor, usamos a chave entre colchetes.
package main
import "fmt"
func main() {
idades := map[string]int{
"Ana": 20,
"Carlos": 25,
}
// Acessa o valor associado à chave "Ana".
fmt.Println(idades["Ana"])
}
Resultado:
20
A chave "Ana" aponta para o valor 20.
Adicionando um novo valor
Para adicionar um novo par chave/valor, usamos =.
package main
import "fmt"
func main() {
idades := map[string]int{
"Ana": 20,
}
// Adiciona uma nova chave no map.
idades["Carlos"] = 25
fmt.Println(idades)
}
Resultado possível:
map[Ana:20 Carlos:25]
Se a chave ainda não existe, o Go cria essa entrada no map.
Removendo um valor com delete
Para remover uma chave do map, usamos delete.
package main
import "fmt"
func main() {
idades := map[string]int{
"Ana": 20,
"Carlos": 25,
}
// Remove a chave "Carlos" do map.
delete(idades, "Carlos")
fmt.Println(idades)
}
Resultado:
map[Ana:20]
A função delete recebe dois argumentos:
delete(map, chave)
Se a chave não existir, nada acontece. Isso não gera erro.
Verificando se uma chave existe
Quando acessamos uma chave que não existe, o Go retorna o valor zero do tipo do valor.
package main
import "fmt"
func main() {
idades := map[string]int{
"Ana": 20,
}
fmt.Println(idades["Carlos"])
}
Resultado:
0
Isso pode confundir, porque 0 pode significar duas coisas:
a chave não existe
a chave existe e o valor dela é 0
Para resolver isso, usamos a forma com dois retornos:
valor, existe := idades["Carlos"]
Exemplo:
package main
import "fmt"
func main() {
idades := map[string]int{
"Ana": 20,
"Carlos": 0,
}
idade, existe := idades["Carlos"]
if existe {
fmt.Println("Idade encontrada:", idade)
} else {
fmt.Println("Usuário não encontrado")
}
}
Resultado:
Idade encontrada: 0
Aqui o valor é 0, mas a chave existe. Por isso o segundo retorno é importante. Esse padrão é muito comum em Go:
valor, ok := mapa[chave]
Muita gente usa o nome ok:
idade, ok := idades["Ana"]
O valor retornado será sempre o valor da chave mais um boolean (true ou false).
Criando map com make
Também podemos criar um map vazio usando make.
package main
import "fmt"
func main() {
// Cria um map vazio.
idades := make(map[string]int)
idades["Ana"] = 20
idades["Carlos"] = 25
fmt.Println(idades)
}
Esse formato é comum quando o map começa vazio e será preenchido depois.
Map vazio e map nil
Existe diferença entre map vazio e map nil. Um map criado com make pode receber valores:
package main
import "fmt"
func main() {
idades := make(map[string]int)
// Funciona.
idades["Ana"] = 20
fmt.Println(idades)
}
Já um map declarado sem inicialização começa como nil.
package main
import "fmt"
func main() {
var idades map[string]int
fmt.Println(idades == nil)
}
Resultado:
true
Um map nil pode ser lido, mas não pode receber novos valores.
package main
func main() {
var idades map[string]int
// Erro em tempo de execução.
// O map está nil, então não podemos adicionar valor.
// idades["Ana"] = 20
_ = idades
}
Para adicionar valores, inicialize com make ou com literal:
idades := make(map[string]int)
// ou:
idades := map[string]int{}
Tipo da chave
A chave de um map precisa ser de um tipo comparável, como string, int, bool e alguns outros podem ser usados como chave.
map[string]int
map[int]string
map[bool]string
Exemplo com int como chave:
package main
import "fmt"
func main() {
usuarios := map[int]string{
1: "Ana",
2: "Carlos",
}
fmt.Println(usuarios[1])
}
Resultado:
Ana
Os Slices não podem ser usados como chave de map, porque slices não são comparáveis diretamente.
Maps com any
O any significa que o valor pode ser de qualquer tipo.
package main
import "fmt"
func main() {
usuario := map[string]any{
"nome": "Ana",
"idade": 20,
"altura": 1.68,
"ativo": true,
}
fmt.Println(usuario["nome"])
fmt.Println(usuario["idade"])
fmt.Println(usuario["altura"])
fmt.Println(usuario["ativo"])
}
Nesse caso, a chave é sempre string, mas o valor pode ser string, int, float64, bool etc.
O cuidado com any
Quando usamos any, o Go deixa o valor entrar, mas depois ele não sabe automaticamente qual é o tipo exato para fazer operações.
package main
import "fmt"
func main() {
dados := map[string]any{
"idade": 20,
}
// Isso não funciona, porque 'dados["idade"]' é any, não int diretamente.
idade := dados["idade"] + 1
fmt.Println(dados["idade"])
}
Para usar como int, precisamos converter com type assertion.
package main
import "fmt"
func main() {
dados := map[string]any{
"idade": 20,
}
// Tenta pegar o valor como int.
idade, ok := dados["idade"].(int)
if !ok {
fmt.Println("idade não é int")
return
}
fmt.Println(idade + 1)
}
Resultado:
21
Verificando tipos diferentes
Quando o valor pode ter vários tipos, usamos switch de tipo.
package main
import "fmt"
func main() {
dados := map[string]any{
"nome": "Ana",
"idade": 20,
"altura": 1.68,
}
for chave, valor := range dados {
switch v := valor.(type) {
case string:
fmt.Println(chave, "é string:", v)
case int:
fmt.Println(chave, "é int:", v)
case float64:
fmt.Println(chave, "é float64:", v)
default:
fmt.Println(chave, "tem tipo desconhecido")
}
}
}
Quando evitar
Se os campos são conhecidos, uma struct normalmente é melhor. Em vez de:
usuario := map[string]any{
"nome": "Ana",
"idade": 20,
}
Prefira:
type Usuario struct {
Nome string
Idade int
}
Exemplo:
package main
import "fmt"
type Usuario struct {
Nome string
Idade int
}
func main() {
usuario := Usuario{
Nome: "Ana",
Idade: 20,
}
fmt.Println(usuario.Nome)
fmt.Println(usuario.Idade)
}
A struct é melhor quando os dados têm formato conhecido, porque o Go consegue verificar os tipos antes do programa rodar.
Sei que ainda não vimos sobre struct, veremos mais adiante no próximo capítulo.