Maps
Em arrays e slices, vimos como armazenar valores em ordem. Agora, vamos descobrir uma forma de armazenar itens por uma key (chave) e procurar por ela rapidamente.
Maps te permitem armazenar itens de forma parecida com a de um dicionário. Você pode pensar na chave como a palavra e o valor como a definição. E tem forma melhor de aprender sobre maps do que criar seu próprio dicionário?
Primeiro, vamos presumir que já temos algumas palavras com suas definições no dicionário. Se procurarmos por uma palavra, o dicionário deve retornar sua definição.

Escreva o teste primeiro

Em dicionario_test.go
1
package main
2
3
import "testing"
4
5
func TestBusca(t *testing.T) {
6
dicionario := map[string]string{"teste": "isso é apenas um teste"}
7
8
resultado := Busca(dicionario, "teste")
9
esperado := "isso é apenas um teste"
10
11
if resultado != esperado {
12
t.Errorf("resultado '%s', esperado '%s', dado '%s'", resultado, esperado, "test")
13
}
14
}
Copied!
Declarar um map é bem parecido com declarar um array. A diferença é que começa com a palavra-chave map e requer dois tipos. O primeiro é o tipo da chave, que é escrito dentro de []. O segundo é o tipo do valor, que vai logo após o [].
O tipo da chave é especial. Só pode ser um tipo comparável, porque sem a habilidade de dizer se duas chaves são iguais, não temos como ter certeza de que estamos obtendo o valor correto. Tipos comparáveis são explicados com detalhes na especificação da linguagem (em inglês).
O tipo do valor, por outro lado, pode ser o tipo que quiser. Pode até ser outro map.
O restante do teste já deve ser familiar para você.

Execute o teste

Ao executar go test, o compilador vai falhar com ./dicionario_test.go:8:9: undefined: Busca.

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

Em dicionario.go:
1
package main
2
3
func Busca(dicionario map[string]string, palavra string) string {
4
return ""
5
}
Copied!
Agora seu teste vai falhar com uma mensagem de erro clara:
dicionario_test.go:12: resultado '', esperado 'isso é apenas um teste', dado 'teste'.

Escreva código o suficiente para fazer o teste passar

1
func Busca(dicionario map[string]string, palavra string) string {
2
return dicionario[palavra]
3
}
Copied!
Obter um valor de um map é igual a obter um valor de um array: map[chave].

Refatoração

1
func TestBusca(t *testing.T) {
2
dicionario := map[string]string{"teste": "isso é apenas um teste"}
3
4
resultado := Busca(dicionario, "teste")
5
esperado := "isso é apenas um teste"
6
7
comparaStrings(t, resultado, esperado)
8
}
9
10
func comparaStrings(t *testing.T, resultado, esperado string) {
11
t.Helper()
12
13
if resultado != esperado {
14
t.Errorf("resultado '%s', esperado '%s', dado '%s'", resultado, esperado, "teste")
15
}
16
}
Copied!
Decidi criar um helper comparaStrings para tornar a implementação mais genérica.

Usando um tipo personalizado

Podemos melhorar o uso do nosso dicionário criando um novo tipo baseado no map e transformando a Busca em um método.
Em dicionario_test.go:
1
func TestBusca(t *testing.T) {
2
dicionario := Dicionario{"teste": "isso é apenas um teste"}
3
4
resultado := dicionario.Busca("teste")
5
esperado := "isso é apenas um teste"
6
7
comparaStrings(t, resultado, esperado)
8
}
Copied!
Começamos a usar o tipo Dicionario, que ainda não definimos. Depois disso, chamamos Busca da instância de Dicionario.
Não precisamos mudar o comparaStrings.
Em dicionario.go:
1
type Dicionario map[string]string
2
3
func (d Dicionario) Busca(palavra string) string {
4
return d[palavra]
5
}
Copied!
Aqui criamos um tipo Dicionario que trabalha em cima da abstração de map. Com o tipo personalizado definido, podemos criar o método Busca.

Escreva o teste primeiro

A busca básica foi bem fácil de implementar, mas o que acontece se passarmos uma palavra que não está no nosso dicionário?
Com o código atual, não recebemos nada de volta. Isso é bom porque o programa continua a ser executado, mas há uma abordagem melhor. A função pode reportar que a palavra não está no dicionário. Dessa forma, o usuário não fica se perguntando se a palavra não existe ou se apenas não existe definição para ela (isso pode não parecer tão útil para um dicionário. No entanto, é um caso que pode ser essencial em outros casos de uso).
1
func TestBusca(t *testing.T) {
2
dicionario := Dicionario{"teste": "isso é apenas um teste"}
3
4
t.Run("palavra conhecida", func(t *testing.T) {
5
resultado, _ := dicionario.Busca("teste")
6
esperado := "isso é apenas um teste"
7
8
comparaStrings(t, resultado, esperado)
9
})
10
11
t.Run("palavra desconhecida", func(t *testing.T) {
12
_, err := dicionario.Busca("desconhecida")
13
14
if err == nil {
15
t.Fatal("é esperado que um erro seja obtido.")
16
}
17
})
18
}
Copied!
A forma de lidar com esse caso no Go é retornar um segundo argumento que é do tipo Error.
Erros podem ser convertidos para uma string com o método .Error(), o que podemos fazer quando passarmos para a asserção. Também estamos protegendo o comparaStrings com if para certificar que não chamemos .Error() quando o erro for nil.

Execute o teste

Isso não vai compilar.
./dicionario_test.go:18:10: assignment mismatch: 2 variables but 1 values
incompatibilidade de atribuição: 2 variáveis, mas 1 valor

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

1
func (d Dicionario) Busca(palavra string) (string, error) {
2
return d[palavra], nil
3
}
Copied!
Agora seu teste deve falhar com uma mensagem de erro muito mais clara.
dicionario_test.go:22: expected to get an error.
erro esperado.

Escreva código o suficiente para fazer o teste passar

1
func (d Dicionario) Busca(palavra string) (string, error) {
2
definicao, existe := d[palavra]
3
if !existe {
4
return "", errors.New("não foi possível encontrar a palavra que você procura")
5
}
6
7
return definicao, nil
8
}
Copied!
Para fazê-lo passar, estamos usando uma propriedade interessante ao percorrer o map. Ele pode retornar dois valores. O segundo valor é uma boleana que indica se a chave foi encontrada com sucesso.
Essa propriedade nos permite diferenciar entre uma palavra que não existe e uma palavra que simplesmente não tem uma definição.

Refatoração

1
var ErrNaoEncontrado = errors.New("não foi possível encontrar a palavra que você procura")
2
3
func (d Dicionario) Busca(palavra string) (string, error) {
4
definicao, existe := d[palavra]
5
if !existe {
6
return "", ErrNaoEncontrado
7
}
8
9
return definicao, nil
10
}
Copied!
Podemos nos livrar do "erro mágico" na nossa função de Busca extraindo-o para dentro de uma variável. Isso também nos permite ter um teste melhor.
1
t.Run("palavra desconhecida", func(t *testing.T) {
2
_, resultado := dicionario.Busca("desconhecida")
3
4
comparaErro(t, resultado, ErrNaoEncontrado)
5
})
6
7
func comparaErro(t *testing.T, resultado, esperado error) {
8
t.Helper()
9
10
if resultado != esperado {
11
t.Errorf("resultado erro '%s', esperado '%s'", resultado, esperado)
12
}
13
}
Copied!
Conseguimos simplificar nosso teste criando um novo helper e começando a usar nossa variável ErrNaoEncontrado para que nosso teste não falhe se mudarmos o texto do erro no futuro.

Escreva o teste primeiro

Temos uma ótima maneira de buscar no dicionário. No entanto, não temos como adicionar novas palavras nele.
1
func TestAdiciona(t *testing.T) {
2
dicionario := Dicionario{}
3
dicionario.Adiciona("teste", "isso é apenas um teste")
4
5
esperado := "isso é apenas um teste"
6
resultado, err := dicionario.Busca("teste")
7
if err != nil {
8
t.Fatal("não foi possível encontrar a palavra adicionada:", err)
9
}
10
11
if esperado != resultado {
12
t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
13
}
14
}
Copied!
Nesse teste, estamos utilizando nossa função Busca para tornar a validação do dicionário um pouco mais fácil.

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

Em dicionario.go
1
func (d Dicionario) Adiciona(palavra, definicao string) {
2
}
Copied!
Agora seu teste deve falhar.
1
dicionario_test.go:31: deveria ter encontrado palavra adicionada: não foi possível encontrar a palavra que você procura
Copied!

Escreva código o suficiente para fazer o teste passar

1
func (d Dicionario) Adiciona(palavra, definicao string) {
2
d[palavra] = definicao
3
}
Copied!
Adicionar coisas a um map também é bem semelhante a um array. Você só precisar especificar uma chave e definir qual é seu valor.

Tipos Referência

Uma propriedade interessante dos maps é que você pode modificá-los sem passá-los como ponteiro. Isso é porque o map é um tipo referência. Isso significa que ele contém uma referência à estrutura de dado que estamos utilizando, assim como um ponteiro. Logo, quando criamos passamos o map como parâmetro, estamos alterando o map original e não sua cópia. A estrutura de dados utilizada é uma tabela de dispersão ou mapa de hash, e você pode ler mais sobre aqui.
É muito bom ter o map como referência, porque não importa o tamanho do map, só vai haver uma cópia.
Um conceito que os tipos referência apresentam é que maps podem ser um valor nil. Um map nil se comporta como um map vazio durante a leitura, mas tentar inserir coisas em um map nil gera um panic em tempo de execução. Você pode saber mais sobre maps aqui (em inglês).
Além disso, você nunca deve inicializar um map vazio, como:
1
var m map[string]string
Copied!
Ao invés disso, você pode inicializar um map vazio como fizemos lá em cima, ou usando a palavra-chave make para criar um map para você:
1
dicionario = map[string]string{}
2
3
// OU
4
5
dicionario = make(map[string]string)
Copied!
Ambas as abordagens criam um hash map vazio e apontam um dicionario para ele. Assim, nos certificamos que você nunca vai obter um panic em tempo de execução.

Refatoração

Não há muito para refatorar na nossa implementação, mas podemos simplificar o teste.
1
func TestAdiciona(t *testing.T) {
2
dicionario := Dicionario{}
3
palavra := "teste"
4
definicao := "isso é apenas um teste"
5
6
dicionario.Adiciona(palavra, definicao)
7
8
comparaDefinicao(t, dicionario, palavra, definicao)
9
}
10
11
func comparaDefinicao(t *testing.T, dicionario Dicionario, palavra, definicao string) {
12
t.Helper()
13
14
resultado, err := dicionario.Busca(palavra)
15
if err != nil {
16
t.Fatal("deveria ter encontrado palavra adicionada:", err)
17
}
18
19
if definicao != resultado {
20
t.Errorf("resultado '%s', esperado '%s'", resultado, definicao)
21
}
22
}
Copied!
Criamos variáveis para palavra e definição e movemos a comparação da definição para sua própria função auxiliar.
Nosso Adiciona está bom. No entanto, não consideramos o que acontece quando o valor que estamos tentando adicionar já existe!
O map não vai mostrar um erro se o valor já existe. Ao invés disso, ele vai sobrescrever o valor com o novo recebido. Isso pode ser conveniente na prática, mas torna o nome da nossa função muito menos preciso. Adiciona não deve modificar valores existentes. Só deve adicionar palavras novas ao nosso dicionário.

Escreva o teste primeiro

1
func TestAdiciona(t *testing.T) {
2
t.Run("palavra nova", func(t *testing.T) {
3
dicionario := Dicionario{}
4
palavra := "teste"
5
definicao := "isso é apenas um teste"
6
7
err := dicionario.Adiciona(palavra, definicao)
8
9
comparaErro(t, err, nil)
10
comparaDefinicao(t, dicionario, palavra, definicao)
11
})
12
13
t.Run("palavra existente", func(t *testing.T) {
14
palavra := "teste"
15
definicao := "isso é apenas um teste"
16
dicionario := Dicionario{palavra: definicao}
17
err := dicionario.Adiciona(palavra, "teste novo")
18
19
comparaErro(t, err, ErrPalavraExistente)
20
comparaDefinicao(t, dicionario, palavra, definicao)
21
})
22
}
Copied!
Para esse teste, fizemos Adiciona devolver um erro, que estamos validando com uma nova variável de erro, ErrPalavraExistente. Também modificamos o teste anterior para verificar um erro nil.

Execute o teste

Agora o compilador vai falhar porque não estamos devolvendo um valor para Adiciona.
1
./dicionario_test.go:30:13: dicionario.Adiciona(palavra, definicao) used as value
2
./dicionario_test.go:41:13: dicionario.Adiciona(palavra, "teste novo") used as value
Copied!
usado como valor

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

Em dicionario.go:
1
var (
2
ErrNaoEncontrado = errors.New("não foi possível encontrar a palavra que você procura")
3
ErrPalavraExistente = errors.New("não é possível adicionar a palavra pois ela já existe")
4
)
5
6
func (d Dicionario) Adiciona(palavra, definicao string) error {
7
d[palavra] = definicao
8
return nil
9
}
Copied!
Agora temos mais dois erros. Ainda estamos modificando o valor e retornando um erro nil.
1
dicionario_test.go:43: resultado erro '%!s(<nil>)', esperado 'não é possível adicionar a palavra pois ela já existe'
2
dicionario_test.go:44: resultado 'teste novo', esperado 'isso é apenas um teste'
Copied!

Escreva código o suficiente para fazer o teste passar

1
func (d Dicionario) Adiciona(palavra, definicao string) error {
2
_, err := d.Busca(palavra)
3
switch err {
4
case ErrNaoEncontrado:
5
d[palavra] = definicao
6
case nil:
7
return ErrPalavraExistente
8
default:
9
return err
10
11
}
12
13
return nil
14
}
Copied!
Aqui estamos usando a declaração switch para coincidir com o erro. Usar o switch dessa forma dá uma segurança a mais, no caso de Busca retornar um erro diferente de ErrNaoEncontrado.

Refatoração

Não temos muito o que refatorar, mas já que nossos erros estão aumentando, podemos fazer algumas modificações.
1
const (
2
ErrNaoEncontrado = ErrDicionario("não foi possível encontrar a palavra que você procura")
3
ErrPalavraExistente = ErrDicionario("não é possível adicionar a palavra pois ela já existe")
4
)
5
6
type ErrDicionario string
7
8
func (e ErrDicionario) Error() string {
9
return string(e)
10
}
Copied!
Tornamos os erros constantes; para isso, tivemos que criar nosso próprio tipo ErrDicionario que implementa a interface error. Você pode ler mais sobre nesse artigo excelente escrito por Dave Cheney (em inglês). Resumindo, isso torna os erros mais reutilizáveis e imutáveis.
Agora, vamos criar uma função que Atualiza a definição de uma palavra.

Escreva o teste primeiro

1
func TestUpdate(t *testing.T) {
2
palavra := "teste"
3
definicao := "isso é apenas um teste"
4
dicionario := Dicionario{palavra: definicao}
5
novaDefinicao := "nova definição"
6
7
dicionario.Atualiza(palavra, novaDefinicao)
8
9
comparaDefinicao(t, dicionario, palavra, novaDefinicao)
10
}
Copied!
Atualiza é bem parecido com Adiciona e será nossa próxima implementação.

Execute o teste

1
./dicionario_test.go:53:2: dicionario.Atualiza undefined (type Dicionario has no field or method Atualiza)
Copied!
dicionario.Atualiza não definido (tipo Dicionario não tem nenhum campo ou método chamado Atualiza

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

Já sabemos como lidar com um erro como esse. Precisamos definir nossa função.
1
func (d Dicionario) Atualiza(palavra, definicao string) {}
Copied!
Feito isso, somos capazes de ver o que precisamos para mudar a definição da palavra.
1
dicionario_test.go:55: resultado 'isso é apenas um teste', esperado 'nova definição'
Copied!

Escreva código o suficiente para fazer o teste passar

Já vimos como fazer essa implementação quando corrigimos o problema com Adiciona. Logo, vamos implementar algo bem parecido com Adiciona.
1
func (d Dicionario) Atualiza(palavra, definicao string) {
2
d[palavra] = definicao
3
}
Copied!
Não é necessário fazer refatorar nada, já que foi uma mudança simples. No entanto, agora temos o mesmo problema com Adiciona. Se passarmos uma palavra nova, Atualiza vai adicioná-la no dicionário.

Escreva o teste primeiro

1
t.Run("palavra existente", func(t *testing.T) {
2
palavra := "teste"
3
definicao := "isso é apenas um teste"
4
novaDefinicao := "nova definição"
5
dicionario := Dicionario{palavra: definicao}
6
err := dicionario.Atualiza(palavra, novaDefinicao)
7
8
comparaErro(t, err, nil)
9
comparaDefinicao(t, dicionario, palavra, novaDefinicao)
10
})
11
12
t.Run("palavra nova", func(t *testing.T) {
13
palavra := "teste"
14
definicao := "isso é apenas um teste"
15
dicionario := Dicionario{}
16
17
err := dicionario.Atualiza(palavra, definicao)
18
19
comparaErro(t, err, ErrPalavraInexistente)
20
})
Copied!
Criamos um outro tipo de erro para quando a palavra não existe. Também modificamos o Atualiza para retornar um valor error.

Execute o teste

1
./dicionario_test.go:53:16: dicionario.Atualiza(palavra, "teste novo") used as value
2
./dicionario_test.go:64:16: dicionario.Atualiza(palavra, definicao) used as value
3
./dicionario_test.go:66:23: undefined: ErrPalavraInexistente
Copied!
Agora recebemos três erros, mas sabemos como lidar com eles.

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

1
const (
2
ErrNaoEncontrado = ErrDicionario("não foi possível encontrar a palavra que você procura")
3
ErrPalavraExistente = ErrDicionario("não é possível adicionar a palavra pois ela já existe")
4
ErrPalavraInexistente = ErrDicionario("não foi possível atualizar a palavra pois ela não existe")
5
)
6
7
func (d Dicionario) Atualiza(palavra, definicao string) error {
8
d[palavra] = definicao
9
return nil
10
}
Copied!
Adicionamos nosso próprio tipo erro e retornamos um erro nil.
Com essas mudanças, agora temos um erro muito mais claro:
1
dicionario_test.go:66: resultado erro '%!s(<nil>)', esperado 'não foi possível atualizar a palavra pois ela não existe'
Copied!

Escreva código o suficiente para fazer o teste passar

1
func (d Dicionario) Atualiza(palavra, definicao string) error {
2
_, err := d.Busca(palavra)
3
switch err {
4
case ErrNaoEncontrado:
5
return ErrPalavraInexistente
6
case nil:
7
d[palavra] = definicao
8
default:
9
return err
10
11
}
12
13
return nil
14
}
Copied!
Essa função é quase idêntica à Adiciona, com exceção de que trocamos quando atualizamos o dicionario e quando retornamos um erro.

Nota sobre a declaração de um novo erro para Atualiza

Poderíamos reutilizar ErrNaoEncontrado e não criar um novo erro. No entanto, geralmente é melhor ter um erro preciso para quando uma atualização falhar.
Ter erros específicos te dá mais informação sobre o que deu errado. Segue um exemplo em uma aplicação web:
Você pode redirecionar o usuário quando o ErrNaoEncontrado é encontrado, mas mostrar uma mensagem de erro só quando ErrPalavraInexistente é encontrado.
Agora, vamos criar uma função que Deleta uma palavra no dicionário.

Escreva o teste primeiro

1
func TestDeleta(t *testing.T) {
2
palavra := "teste"
3
dicionario := Dicionario{palavra: "definição de teste"}
4
5
dicionario.Deleta(palavra)
6
7
_, err := dicionario.Busca(palavra)
8
if err != ErrNaoEncontrado {
9
t.Errorf("espera-se que '%s' seja deletado", palavra)
10
}
11
}
Copied!
Nosso teste cria um Dicionario com uma palavra e depois verifica se a palavra foi removida.

Execute o teste

Executando go test obtemos:
1
./dicionario_test.go:74:6: dicionario.Deleta undefined (type Dicionario has no field or method Deleta)
Copied!
dicionario.Deleta não definido (tipo Dicionario não tem campo ou método Deleta)

Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado

1
func (d Dicionario) Deleta(palavra string) {
2
3
}
Copied!
Depois que adicionamos isso, o teste nos diz que não estamos deletando a palavra.
1
dicionario_test.go:78: espera-se que 'teste' seja deletado
Copied!

Escreva código o suficiente para fazer o teste passar

1
func (d Dicionario) Deleta(palavra string) {
2
delete(d, palavra)
3
}
Copied!
Go tem uma função nativa chamada delete que funciona em maps. Ela leva dois argumentos: o primeiro é o map e o segundo é a chave a ser removida.
A função delete não retorna nada, e baseamos nosso método Deleta nesse conceito. Já que deletar um valor não tem nenhum efeito, diferentemente dos nossos métodos Atualiza e Adiciona, não precisamos complicar a API com erros.

Resumo

Nessa seção, falamos sobre muita coisa. Criamos uma API CRUD (Criar, Ler, Atualizar e Deletar) completa para nosso dicionário. No decorrer do processo, aprendemos como:
    Criar maps
    Buscar por itens em maps
    Adicionar novos itens aos maps
    Atualizar itens em maps
    Deletar itens de um map
    Aprendemos mais sobre erros
      Como criar erros que são constantes
      Escrever encapsuladores de erro
Last modified 9mo ago
Copy link
Contents
Escreva o teste primeiro
Execute o teste
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
Escreva código o suficiente para fazer o teste passar
Refatoração
Usando um tipo personalizado
Escreva o teste primeiro
Execute o teste
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
Escreva código o suficiente para fazer o teste passar
Refatoração
Escreva o teste primeiro
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
Escreva código o suficiente para fazer o teste passar
Tipos Referência
Refatoração
Escreva o teste primeiro
Execute o teste
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
Escreva código o suficiente para fazer o teste passar
Refatoração
Escreva o teste primeiro
Execute o teste
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
Escreva código o suficiente para fazer o teste passar
Escreva o teste primeiro
Execute o teste
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
Escreva código o suficiente para fazer o teste passar
Nota sobre a declaração de um novo erro para Atualiza
Escreva o teste primeiro
Execute o teste
Escreva o mínimo de código possível para fazer o teste rodar e verifique a saída do teste que tiver falhado
Escreva código o suficiente para fazer o teste passar
Resumo