Arrays e slices
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.
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.Ao executar
go test
, o compilador vai falhar com ./soma_test.go:10:15: undefined: Soma
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]
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
.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.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.
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)
}
})
}
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
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.
Nesse caso, para arrumar os problemas de compilação, tudo o que precisamos fazer aqui é fazer os testes passarem!
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}
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)
}
}
./soma_test.go:23:9: undefined: SomaTudo
Precisamos definir o SomaTudo de acordo com o que nosso teste precisa.
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]
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.
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").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)
}
}
./soma_test.go:26:9: undefined: SomaTodoOResto
Renomeie a função para
SomaTodoOResto
e volte a executar o teste.soma_test.go:30: resultado [3 9], esperado [2 9]
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.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:]
?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)
}
})
}
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.
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
}
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
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á.