Maps
Last updated
Was this helpful?
Last updated
Was this helpful?
Em , 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.
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 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ê.
Ao executar go test
, o compilador vai falhar com ./dicionario_test.go:8:9: undefined: Busca
.
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'
.
Obter um valor de um map é igual a obter um valor de um array: map[chave]
.
Decidi criar um helper comparaStrings
para tornar a implementação mais genérica.
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
.
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
.
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
Agora seu teste deve falhar com uma mensagem de erro muito mais clara.
dicionario_test.go:22: expected to get an error.
erro esperado.
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.
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.
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.
Em dicionario.go
Agora seu teste deve falhar.
Adicionar coisas a um map também é bem semelhante a um array. Você só precisar especificar uma chave e definir qual é seu valor.
É muito bom ter o map como referência, porque não importa o tamanho do map, só vai haver uma cópia.
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.
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.
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
.
Agora o compilador vai falhar porque não estamos devolvendo um valor para Adiciona
.
usado como valor
Em dicionario.go
:
Agora temos mais dois erros. Ainda estamos modificando o valor e retornando um erro nil
.
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
.
Não temos muito o que refatorar, mas já que nossos erros estão aumentando, podemos fazer algumas modificações.
Agora, vamos criar uma função que Atualiza
a definição de uma palavra.
Atualiza
é bem parecido com Adiciona
e será nossa próxima implementação.
dicionario.Atualiza não definido (tipo Dicionario não tem nenhum campo ou método chamado Atualiza
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.
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.
Criamos um outro tipo de erro para quando a palavra não existe. Também modificamos o Atualiza
para retornar um valor error
.
Agora recebemos três erros, mas sabemos como lidar com eles.
Adicionamos nosso próprio tipo erro e retornamos um erro nil
.
Com essas mudanças, agora temos um erro muito mais claro:
Essa função é quase idêntica à Adiciona
, com exceção de que trocamos quando atualizamos o dicionario
e quando retornamos um erro.
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.
Nosso teste cria um Dicionario
com uma palavra e depois verifica se a palavra foi removida.
Executando go test
obtemos:
dicionario.Deleta não definido (tipo Dicionario não tem campo ou método Deleta)
Depois que adicionamos isso, o teste nos diz que não estamos deletando a palavra.
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.
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
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 (em inglês).
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 .
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 (em inglês).
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 (em inglês). Resumindo, isso torna os erros mais reutilizáveis e imutáveis.