Arrays e slices

Você pode encontrar todos os códigos para esse capítulo aqui

Arrays te permitem armazenar diversos elementos do mesmo tipo em uma variável em uma ordem específica.

Quando você tem um array, é muito comum ter que percorrer sobre ele. Logo, vamos usar nosso recém adquirido conhecimento de for para criar uma função Soma. Soma vai receber um array de números e retornar o total.

Também vamos praticar nossas habilidades em TDD.

Escreva o teste primeiro

Em soma_test.go:

package main
import "testing"
func TestSoma(t *testing.T) {
numeros := [5]int{1, 2, 3, 4, 5}
resultado := Soma(numeros)
esperado := 15
if esperado != resultado {
t.Errorf("resultado %d, esperado %d, dado %v", resultado, esperado, numeros)
}
}

Arrays têm uma capacidade fixa que é definida quando você declara a variável. Podemos inicializar um array de duas formas:

  • [N]tipo{valor1, valor2, ..., valorN}, como numeros := [5]int{1, 2, 3, 4, 5}

  • [...]tipo{valor1, valor2, ..., valorN}, como numbers := [...]int{1, 2, 3, 4, 5}

Às vezes é útil também mostrarmos as entradas da função na mensagem de erro. Para isso estamos usando o formatador %v, que é o formato "padrão" e funciona bem com arrays.

Leia mais sobre formatação de strings aqui

Execute o teste

Ao executar go test, o compilador vai falhar com ./soma_test.go:10:15: undefined: Soma

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

Em soma.go:

package main
func Soma(numeros [5]int) int {
return 0
}

Agora seu teste deve falhar com uma mensagem clara de erro:

soma_test.go:13: resultado 0, esperado 15, dado [1 2 3 4 5]

Escreva código o suficiente para fazer o teste passar

func Soma(numeros [5]int) int {
soma := 0
for i := 0; i < 5; i++ {
soma += numeros[i]
}
return soma
}

Para receber o valor de um array em uma posição específica, basta usar a sintaxe array[índice]. Nesse caso, estamos usando o for para percorrer cada posição do array (que tem 5 posições) e somar cada valor na variável soma.

Refatoração

Vamos apresentar o range para nos ajudar a limpar o código:

func Soma(numeros [5]int) int {
soma := 0
for _, numero := range numeros {
soma += numero
}
return soma
}

O range permite que você percorra um array. Sempre que é chamado, retorna dois valores: o índice e o valor. Decidimos ignorar o valor índice usando _ blank identifier.

Arrays e seus tipos

Uma propriedade interessante dos arrays é que seu tamanho é relacionado ao seu tipo. Se tentar passar um [4]int dentro da função que espera [5]int, ela não vai compilar. Elas são de tipos diferentes e é a mesma coisa que tentar passar uma string para uma função que espera um int.

Você pode estar pensando que é bastante complicado que arrays tenham tamanho fixo, não é? Só que na maioria das vezes, você provavelmente não vai usá-los!

O Go tem slices, em que você não define o tamanho da coleção e, graças a isso, pode ter qualquer tamanho.

O próprio requerimento será somar coleções de tamanhos variados.

Escreva o teste primeiro

Agora vamos usar o tipo slice que nos permite ter coleções de qualquer tamanho. A sintaxe é bem parecida com a dos arrays e você só precisa omitir o tamanho quando declará-lo.

meuSlice := []int{1,2,3} ao invés de meuArray := [3]int{1,2,3}

func TestSoma(t *testing.T) {
t.Run("coleção de 5 números", func(t *testing.T) {
numeros := [5]int{1, 2, 3, 4, 5}
resultado := Soma(numeros)
esperado := 15
if resultado != esperado {
t.Errorf("resultado %d, want %d, dado %v", resultado, esperado, numeros)
}
})
t.Run("coleção de qualquer tamanho", func(t *testing.T) {
numeros := []int{1, 2, 3}
resultado := Soma(numeros)
esperado := 6
if resultado != esperado {
t.Errorf("resultado %d, esperado %d, dado %v", resultado, esperado, numeros)
}
})
}

Execute o teste

Isso não vai compilar.

./soma_test.go:22:13: cannot use numbers (type []int) as type [5]int in argument to Soma

não é possível usar números (tipo []int) como tipo [5]int no argumento para Soma

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

Para resolver o problema, podemos:

  • Alterar a API existente mudando o argumento de Soma para um slice ao invés de um array.Quando fazemos isso, vamos saber que podemos ter arruinado do dia de alguém, porque nosso outro teste não vai compilar!

  • Criar uma nova função

No nosso caso, mais ninguém está usando nossa função. Logo, ao invés de ter duas funções para manter, vamos usar apenas uma.

func Soma(numeros []int) int {
soma := 0
for _, numero := range numeros {
soma += numero
}
return soma
}

Se tentar rodar os testes eles ainda não vão compilar. Você vai ter que alterar o primeiro teste e passar um slice ao invés de um array.

Escreva código o suficiente para fazer o teste passar

Nesse caso, para arrumar os problemas de compilação, tudo o que precisamos fazer aqui é fazer os testes passarem!

Refatoração

Nós já refatoramos a função Soma e tudo o que fizemos foi mudar os arrays para slices. Logo, não há muito o que fazer aqui. Lembre-se que não devemos abandonar nosso código de teste na etapa de refatoração e precisamos fazer alguma coisa aqui.

func TestSoma(t *testing.T) {
t.Run("coleção de 5 números", func(t *testing.T) {
numeros := []int{1, 2, 3, 4, 5}
resultado := Soma(numeros)
esperado := 15
if resultado != esperado {
t.Errorf("resultado %d, esperado %d, dado, %v", resultado, esperado, numeros)
}
})
t.Run("coleção de qualquer tamanho", func(t *testing.T) {
numeros := []int{1, 2, 3}
resultado := Soma(numeros)
esperado := 6
if resultado != esperado {
t.Errorf("resultado %d, esperado %d, dado %v", resultado, esperado, numeros)
}
})
}

É importante questionar o valor dos seus testes. Ter o máximo de testes possível não deve ser o objetivo e sim ter o máximo de confiança possível na sua base de código. Ter testes demais pode se tornar um problema real e só adiciona mais peso na manutenção. Todo teste tem um custo.

No nosso caso, dá para perceber que ter dois testes para essa função é redundância. Se funciona para um slice de determindo tamanho, é muito provável que funciona para um slice de qualquer tamanho (dentro desse escopo).

A ferramenta de testes nativa do Go tem a funcionalidade de cobertura de código que te ajuda a identificar áreas do seu código que você não cobriu. Já adianto que ter 100% de cobertura não deve ser seu objetivo; é apenas uma ferramenta para te dar uma ideia da sua cobertura. De qualquer forma, se você aplicar o TDD, é bem provável que chegue bem perto dos 100% de cobertura.

Tente executar go test -cover no terminal.

Você deve ver:

PASS
coverage: 100.0% of statements

Agora apague um dos testes e verifique a cobertura novamente.

Agora que estamos felizes com nossa função bem testada, você deve salvar seu trabalho incrível com um commit antes de partir para o próximo desafio.

Precisamos de uma nova função chamada SomaTudo, que vai receber uma quantidade variável de slices e devolver um novo slice contendo as somas de cada slice recebido.

Por exemplo:

SomaTudo([]int{1,2}, []int{0,9}) deve retornar []int{3, 9}

ou

SomaTudo([]int{1,1,1}) deve retornar []int{3}

Escreva o teste primeiro

func TestSomaTudo(t *testing.T) {
resultado := SomaTudo([]int{1,2}, []int{0,9})
esperado := []int{3, 9}
if resultado != esperado {
t.Errorf("resultado %v esperado %v", resultado, esperado)
}
}

Execute o teste

./soma_test.go:23:9: undefined: SomaTudo

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

Precisamos definir o SomaTudo de acordo com o que nosso teste precisa.

O Go te permite escrever funções variádicas em que a quantidade de argumentos podem variar.

func SomaTudo(numerosParaSomar ...[]int) (somas []int) {
return
}

Pode tentar compilar, mas nossos testes não vão funcionar!

./soma_test.go:26:9: invalid operation: got != want (slice can only be compared to nil)

operação inválida: recebido != esperado (slice só pode ser comparado a nil

O Go não te deixa usar operadores de igualdade com slices. É possível escrever uma função que percorre cada slice recebido e esperado e verificar seus valores, mas por praticidade podemos usar o reflect.DeepEqual que é útil para verificar se duas variáveis são iguais.

func TestSomaTudo(t *testing.T) {
recebido := SomaTudo([]int{1,2}, []int{0,9})
esperado := []int{3, 9}
if !reflect.DeepEqual(recebido, esperado) {
t.Errorf("recebido %v esperado %v", recebido, esperado)
}
}

(coloque import reflect no topo do seu arquivo para ter acesso ao DeepEqual)

É importante saber que o reflect.DeepEqual não tem "segurança de tipos", ou seja, o código vai compilar mesmo se você tiver feito algo estranho. Para ver isso em ação, altere o teste temporariamente para:

func TestSomaTudo(t *testing.T) {
recebido := SomaTudo([]int{1,2}, []int{0,9})
esperado := "joao"
if !reflect.DeepEqual(recebido, esperado) {
t.Errorf("recebido %v, esperado %v", recebido, esperado)
}
}

O que fizemos aqui foi comparar um slice com uma string. Isso não faz sentido, mas o teste compila! Logo, apesar de ser uma forma simples de comparar slices (e outras coisas), você deve tomar cuidado quando for usar o reflect.DeepEqual.

Volte o teste da forma como estava e execute-o. Você deve ter a saída do teste com uma mensagem tipo:

soma_test.go:30: recebido [], esperado [3 9]

Escreva código o suficiente para fazer o teste passar

O que precisamos fazer é percorrer as variáveis recebidas como argumento, calcular a soma com nossa função Soma de antes e adicioná-la ao slice que vamos retornar:

func SomaTudo(numerosParaSomar ...[]int) (somas []int) {
quantidadeDeNumeros := len(numerosParaSomar)
somas = make([]int, quantidadeDeNumeros)
for i, numeros := range numerosParaSomar {
somas[i] = Soma(numeros)
}
return
}

Muitas coisas novas para aprender!

Há uma nova forma de criar um slice. O make te permite criar um slice com uma capacidade inicial de len de numerosParaSomar que precisamos percorrer.

Você pode indexar slices como arrays com meuSlice[N] para obter seu valor ou designá-lo a um novo valor com =.

Agora o teste deve passar.

Refatoração

Como mencionado, slices têm uma capacidade. Se você tiver um slice com uma capacidade de 2 e tentar fazer uma atribuição como meuSlice[10] = 1, vai receber um erro em tempo de execução.

No entanto, você pode usar a função append, que recebe um slice e um novo valor e retorna um novo slice com todos os itens dentro dele.

func SomaTudo(numerosParaSomar ...[]int) []int {
var somas []int
for _, numeros := range numerosParaSomar {
somas = append(somas, Soma(numeros))
}
return somas
}

Nessa implementação, nos preocupamos menos sobre capacidade. Começamos com um slice vazio somas e o anexamos ao resultado de Soma enquanto percorremos as variáveis recebidas como argumento.

Nosso próprio requisito é alterar o SomaTudo para SomaTodoOResto, onde agora calcula os totais de todos os "finais" de cada slice. O final de uma coleção é todos os itens com exceção do primeiro (a "cabeça").

Escreva o teste primeiro

func TestSomaTodoOResto(t *testing.T) {
resultado := SomaTodoOResto([]int{1,2}, []int{0,9})
esperado := []int{2, 9}
if !reflect.DeepEqual(resultado, esperado) {
t.Errorf("resultado %v, esperado %v", resultado, esperado)
}
}

Execute o teste

./soma_test.go:26:9: undefined: SomaTodoOResto

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

Renomeie a função para SomaTodoOResto e volte a executar o teste.

soma_test.go:30: resultado [3 9], esperado [2 9]

Escreva código o suficiente para fazer o teste passar

func SomaTodoOResto(numerosParaSomar ...[]int) []int {
var somas []int
for _, numeros := range numerosParaSomar {
final := numeros[1:]
somas = append(somas, Soma(final))
}
return somas
}

Slices podem ser "fatiados"! A sintaxe usada é slice[inicio:final]. Se você omitir o valor de um dos lados dos : ele captura tudo do lado omitido. No nosso caso, quando usamos numeros[1:], estamos dizendo "pegue da posição 1 até o final". É uma boa ideia investir um tempo escrevend outros testes com slices e brincar com o operador slice para criar mais familiaridade com ele.

Refatoração

Não tem muito o que refatorar dessa vez.

O que acha que aconteceria se você passar um slice vazio para a nossa função? Qual é o "final" de um slice vazio? O que acontece quando você fala para o Go capturar todos os elementos de meuSliceVazio[1:]?

Escreva o teste primeiro

func TestSomaTodoOResto(t *testing.T) {
t.Run("faz as somas de alguns slices", func(t *testing.T) {
resultado := SomaTodoOResto([]int{1,2}, []int{0,9})
esperado := []int{2, 9}
if !reflect.DeepEqual(resultado, esperado) {
t.Errorf("resultado %v, esperado %v", resultado, esperado)
}
})
t.Run("soma slices vazios de forma segura", func(t *testing.T) {
resultado := SomaTodoOResto([]int{}, []int{3, 4, 5})
esperado := []int{0, 9}
if !reflect.DeepEqual(resultado, esperado) {
t.Errorf("resultado %v, esperado %v", resultado, esperado)
}
})
}

Execute o teste

panic: runtime error: slice bounds out of range [recovered]
panic: runtime error: slice bounds out of range

pânico: erro em tempo de execução: fora da capacidade do slice

Oh, não! É importante perceber que o test foi compilado, esse é um erro em tempo de execução. Erros em tempo de compilação são nossos amigos, porque nos ajudam a escrever softwares que funcionam. Erros em tempo de execução são nosso inimigos, porque afetam nossos usuários.

Escreva código o suficiente para fazer o teste passar

func SomaTodoOResto(numerosParaSomar ...[]int) []int {
var somas []int
for _, numeros := range numerosParaSomar {
if len(numeros) == 0 {
somas = append(somas, 0)
} else {
final := numeros[1:]
somas = append(somas, Soma(final))
}
}
return somas
}

Refatoração

Nossos testes têm código repetido em relação à asserção de novo. Vamos encapsular isso em uma função:

func TestSomaTodoOResto(t *testing.T) {
verificaSomas := func(t *testing.T, resultado, esperado []int) {
t.Helper()
if !reflect.DeepEqual(resultado, esperado) {
t.Errorf("resultado %v, esperado %v", resultado, esperado)
}
}
t.Run("faz a soma do resto", func(t *testing.T) {
resultado := SomaTodoOResto([]int{1, 2}, []int{0, 9})
esperado := []int{2, 9}
verificaSomas(t, resultado, esperado)
})
t.Run("soma slices vazios de forma segura", func(t *testing.T) {
resultado := SomaTodoOResto([]int{}, []int{3, 4, 5})
esperado := []int{0, 9}
verificaSomas(t, resultado, esperado)
})
}

Um efeito colateral útil disso é que adiciona um pouco de segurança de tipos no nosso código. Se uma pessoa espertinha adicionar um novo teste com verificaSomas(t, resultado, "luisa") o compilador vai pará-lo antes que algo errado aconteça.

$ go test
./soma_test.go:52:21: cannot use "luisa" (type string) as type []int in argument to verificaSomas

não é possível usar "luisa" (tipo string) como tipo []int no argumento para verificaSomas

Resumindo

Falamos sobre:

  • Arrays

  • Slices

  • Várias formas de criá-las

  • Como eles têm uma capacidade fixa, mas é posível criar novos slices de antigos usando append

  • Como "fatiar" slices!

  • len obtém o tamanho de um array ou slice

  • Ferramenta de cobertura de testes

  • reflect.DeepEqual e por que é útil, mas pode diminuir a segurança de tipos do seu código

Usamos slices e arrays com inteiros, mas eles também funcionam com qualquer outro tipo, incluindo até os próprios arrays/slices. Logo, você pode declarar uma variável de [][]string se precisar.

Dê uma olhada no post sobre slices no blog de Go para saber mais sobre slices. Tente escrever mais testes para demonstrar o que você aprendeu com a leitura.

Outra forma útil de brincar com Go ao invés de escrever testes é o Go playground. Você pode testar mais coisas lá e você pode compartilhar seu código facilmente se precisar tirar dúvidas. Criei um exemplo com um slice para testar lá.