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

package main

import "testing"

func TestBusca(t *testing.T) {
    dicionario := map[string]string{"teste": "isso é apenas um teste"}

    resultado := Busca(dicionario, "teste")
    esperado := "isso é apenas um teste"

    if resultado != esperado {
        t.Errorf("resultado '%s', esperado '%s', dado '%s'", resultado, esperado, "test")
    }
}

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:

package main

func Busca(dicionario map[string]string, palavra string) string {
    return ""
}

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

func Busca(dicionario map[string]string, palavra string) string {
    return dicionario[palavra]
}

Obter um valor de um map é igual a obter um valor de um array: map[chave].

Refatoração

func TestBusca(t *testing.T) {
    dicionario := map[string]string{"teste": "isso é apenas um teste"}

    resultado := Busca(dicionario, "teste")
    esperado := "isso é apenas um teste"

    comparaStrings(t, resultado, esperado)
}

func comparaStrings(t *testing.T, resultado, esperado string) {
    t.Helper()

    if resultado != esperado {
        t.Errorf("resultado '%s', esperado '%s', dado '%s'", resultado, esperado, "teste")
    }
}

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:

func TestBusca(t *testing.T) {
    dicionario := Dicionario{"teste": "isso é apenas um teste"}

    resultado := dicionario.Busca("teste")
    esperado := "isso é apenas um teste"

    comparaStrings(t, resultado, esperado)
}

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:

type Dicionario map[string]string

func (d Dicionario) Busca(palavra string) string {
    return d[palavra]
}

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).

func TestBusca(t *testing.T) {
    dicionario := Dicionario{"teste": "isso é apenas um teste"}

    t.Run("palavra conhecida", func(t *testing.T) {
        resultado, _ := dicionario.Busca("teste")
        esperado := "isso é apenas um teste"

        comparaStrings(t, resultado, esperado)
    })

    t.Run("palavra desconhecida", func(t *testing.T) {
        _, err := dicionario.Busca("desconhecida")

        if err == nil {
            t.Fatal("é esperado que um erro seja obtido.")
        }
    })
}

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

func (d Dicionario) Busca(palavra string) (string, error) {
    return d[palavra], nil
}

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

func (d Dicionario) Busca(palavra string) (string, error) {
    definicao, existe := d[palavra]
    if !existe {
        return "", errors.New("não foi possível encontrar a palavra que você procura")
    }

    return definicao, nil
}

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

var ErrNaoEncontrado = errors.New("não foi possível encontrar a palavra que você procura")

func (d Dicionario) Busca(palavra string) (string, error) {
    definicao, existe := d[palavra]
    if !existe {
        return "", ErrNaoEncontrado
    }

    return definicao, nil
}

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.

t.Run("palavra desconhecida", func(t *testing.T) {
    _, resultado := dicionario.Busca("desconhecida")

    comparaErro(t, resultado, ErrNaoEncontrado)
})

func comparaErro(t *testing.T, resultado, esperado error) {
    t.Helper()

    if resultado != esperado {
        t.Errorf("resultado erro '%s', esperado '%s'", resultado, esperado)
    }
}

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.

func TestAdiciona(t *testing.T) {
    dicionario := Dicionario{}
    dicionario.Adiciona("teste", "isso é apenas um teste")

    esperado := "isso é apenas um teste"
    resultado, err := dicionario.Busca("teste")
    if err != nil {
        t.Fatal("não foi possível encontrar a palavra adicionada:", err)
    }

    if esperado != resultado {
        t.Errorf("resultado '%s', esperado '%s'", resultado, esperado)
    }
}

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

func (d Dicionario) Adiciona(palavra, definicao string) {
}

Agora seu teste deve falhar.

dicionario_test.go:31: deveria ter encontrado palavra adicionada: não foi possível encontrar a palavra que você procura

Escreva código o suficiente para fazer o teste passar

func (d Dicionario) Adiciona(palavra, definicao string) {
    d[palavra] = definicao
}

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:

var m map[string]string

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ê:

dicionario = map[string]string{}

// OU

dicionario = make(map[string]string)

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.

func TestAdiciona(t *testing.T) {
    dicionario := Dicionario{}
    palavra := "teste"
    definicao := "isso é apenas um teste"

    dicionario.Adiciona(palavra, definicao)

    comparaDefinicao(t, dicionario, palavra, definicao)
}

func comparaDefinicao(t *testing.T, dicionario Dicionario, palavra, definicao string) {
    t.Helper()

    resultado, err := dicionario.Busca(palavra)
    if err != nil {
        t.Fatal("deveria ter encontrado palavra adicionada:", err)
    }

    if definicao != resultado {
        t.Errorf("resultado '%s',  esperado '%s'", resultado, definicao)
    }
}

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

func TestAdiciona(t *testing.T) {
    t.Run("palavra nova", func(t *testing.T) {
        dicionario := Dicionario{}
        palavra := "teste"
        definicao := "isso é apenas um teste"

        err := dicionario.Adiciona(palavra, definicao)

        comparaErro(t, err, nil)
        comparaDefinicao(t, dicionario, palavra, definicao)
    })

    t.Run("palavra existente", func(t *testing.T) {
        palavra := "teste"
        definicao := "isso é apenas um teste"
        dicionario := Dicionario{palavra: definicao}
        err := dicionario.Adiciona(palavra, "teste novo")

        comparaErro(t, err, ErrPalavraExistente)
        comparaDefinicao(t, dicionario, palavra, definicao)
    })
}

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.

./dicionario_test.go:30:13: dicionario.Adiciona(palavra, definicao) used as value
./dicionario_test.go:41:13: dicionario.Adiciona(palavra, "teste novo") used as value

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:

var (
    ErrNaoEncontrado = errors.New("não foi possível encontrar a palavra que você procura")
    ErrPalavraExistente = errors.New("não é possível adicionar a palavra pois ela já existe")
)

func (d Dicionario) Adiciona(palavra, definicao string) error {
    d[palavra] = definicao
    return nil
}

Agora temos mais dois erros. Ainda estamos modificando o valor e retornando um erro nil.

dicionario_test.go:43: resultado erro '%!s(<nil>)', esperado 'não é possível adicionar a palavra pois ela já existe'
dicionario_test.go:44: resultado 'teste novo', esperado 'isso é apenas um teste'

Escreva código o suficiente para fazer o teste passar

func (d Dicionario) Adiciona(palavra, definicao string) error {
    _, err := d.Busca(palavra)
    switch err {
    case ErrNaoEncontrado:
        d[palavra] = definicao
    case nil:
        return ErrPalavraExistente
    default:
        return err

    }

    return 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.

Refatoração

Não temos muito o que refatorar, mas já que nossos erros estão aumentando, podemos fazer algumas modificações.

const (
    ErrNaoEncontrado = ErrDicionario("não foi possível encontrar a palavra que você procura")
    ErrPalavraExistente = ErrDicionario("não é possível adicionar a palavra pois ela já existe")
)

type ErrDicionario string

func (e ErrDicionario) Error() string {
    return string(e)
}

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

func TestUpdate(t *testing.T) {
    palavra := "teste"
    definicao := "isso é apenas um teste"
    dicionario := Dicionario{palavra: definicao}
    novaDefinicao := "nova definição"

    dicionario.Atualiza(palavra, novaDefinicao)

    comparaDefinicao(t, dicionario, palavra, novaDefinicao)
}

Atualiza é bem parecido com Adiciona e será nossa próxima implementação.

Execute o teste

./dicionario_test.go:53:2: dicionario.Atualiza undefined (type Dicionario has no field or method Atualiza)

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.

func (d Dicionario) Atualiza(palavra, definicao string) {}

Feito isso, somos capazes de ver o que precisamos para mudar a definição da palavra.

dicionario_test.go:55: resultado 'isso é apenas um teste', esperado 'nova definição'

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.

func (d Dicionario) Atualiza(palavra, definicao string) {
    d[palavra] = definicao
}

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

    t.Run("palavra existente", func(t *testing.T) {
        palavra := "teste"
        definicao := "isso é apenas um teste"
        novaDefinicao := "nova definição"
        dicionario := Dicionario{palavra: definicao}
        err := dicionario.Atualiza(palavra, novaDefinicao)

        comparaErro(t, err, nil)
        comparaDefinicao(t, dicionario, palavra, novaDefinicao)
    })

    t.Run("palavra nova", func(t *testing.T) {
        palavra := "teste"
        definicao := "isso é apenas um teste"
        dicionario := Dicionario{}

        err := dicionario.Atualiza(palavra, definicao)

        comparaErro(t, err, ErrPalavraInexistente)
    })

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

./dicionario_test.go:53:16: dicionario.Atualiza(palavra, "teste novo") used as value
./dicionario_test.go:64:16: dicionario.Atualiza(palavra, definicao) used as value
./dicionario_test.go:66:23: undefined: ErrPalavraInexistente

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

const (
    ErrNaoEncontrado = ErrDicionario("não foi possível encontrar a palavra que você procura")
    ErrPalavraExistente = ErrDicionario("não é possível adicionar a palavra pois ela já existe")
    ErrPalavraInexistente = ErrDicionario("não foi possível atualizar a palavra pois ela não existe")
)

func (d Dicionario) Atualiza(palavra, definicao string) error {
    d[palavra] = definicao
    return nil
}

Adicionamos nosso próprio tipo erro e retornamos um erro nil.

Com essas mudanças, agora temos um erro muito mais claro:

dicionario_test.go:66: resultado erro '%!s(<nil>)', esperado 'não foi possível atualizar a palavra pois ela não existe'

Escreva código o suficiente para fazer o teste passar

func (d Dicionario) Atualiza(palavra, definicao string) error {
    _, err := d.Busca(palavra)
    switch err {
    case ErrNaoEncontrado:
        return ErrPalavraInexistente
    case nil:
        d[palavra] = definicao
    default:
        return err

    }

    return nil
}

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

func TestDeleta(t *testing.T) {
    palavra := "teste"
    dicionario := Dicionario{palavra: "definição de teste"}

    dicionario.Deleta(palavra)

    _, err := dicionario.Busca(palavra)
    if err != ErrNaoEncontrado {
        t.Errorf("espera-se que '%s' seja deletado", palavra)
    }
}

Nosso teste cria um Dicionario com uma palavra e depois verifica se a palavra foi removida.

Execute o teste

Executando go test obtemos:

./dicionario_test.go:74:6: dicionario.Deleta undefined (type Dicionario has no field or method Deleta)

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

func (d Dicionario) Deleta(palavra string) {

}

Depois que adicionamos isso, o teste nos diz que não estamos deletando a palavra.

dicionario_test.go:78: espera-se que 'teste' seja deletado

Escreva código o suficiente para fazer o teste passar

func (d Dicionario) Deleta(palavra string) {
    delete(d, 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.

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