Maps
Você pode encontrar todos os códigos para esse capítulo aqui
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
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
:
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
Obter um valor de um map é igual a obter um valor de um array: map[chave]
.
Refatoração
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
:
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
:
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).
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
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
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
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.
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.
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
Agora seu teste deve falhar.
Escreva código o suficiente para fazer o teste passar
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:
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ê:
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.
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
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
.
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
:
Agora temos mais dois erros. Ainda estamos modificando o valor e retornando um erro nil
.
Escreva código o suficiente para fazer o teste passar
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.
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
Atualiza
é bem parecido com Adiciona
e será nossa próxima implementação.
Execute o teste
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.
Feito isso, somos capazes de ver o que precisamos para mudar a definição da palavra.
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
.
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
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
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
Adicionamos nosso próprio tipo erro e retornamos um erro nil
.
Com essas mudanças, agora temos um erro muito mais claro:
Escreva código o suficiente para fazer o teste passar
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ó quandoErrPalavraInexistente
é encontrado.
Agora, vamos criar uma função que Deleta
uma palavra no dicionário.
Escreva o teste primeiro
Nosso teste cria um Dicionario
com uma palavra e depois verifica se a palavra foi removida.
Execute o teste
Executando go test
obtemos:
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
Depois que adicionamos isso, o teste nos diz que não estamos deletando a palavra.
Escreva código o suficiente para fazer o teste passar
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 updated