Entendendo matrizes e fatias em Go

Introdução

Na linguagem Go, matrizes e fatias são estruturas de dados que consistem em uma sequência ordenada de elementos. Essas coleções de dados são ótimas para serem usadas quando você quiser trabalhar com muitos valores relacionados. Elas permitem que você: mantenha juntos dados que devam permanecer juntos; que condense o seu código e execute os mesmos métodos e operações em vários valores – tudo ao mesmo tempo.

Embora matrizes e fatias sejam ambas sequências ordenadas de elementos em Go, há diferenças significativas entre as duas. Na linguagem Go, uma matriz é uma estrutura de dados que consiste em uma sequência ordenada de elementos que tem sua capacidade definida no momento de sua criação. Assim que o tamanho de uma matriz tiver sido alocado, o tamanho não mais poderá ser alterado. Uma fatia, por outro lado, é uma versão da matriz que admite variação de comprimento, proporcionando mais flexibilidade para os desenvolvedores que usam essas estruturas de dados. As fatias se constituem no que poderíamos considerar como sendo matrizes em outras linguagens.

Dadas essas diferenças, existem situações específicas em que seria melhor usar uma em vez da outra. Se você é novato na linguagem de programação Go, determinar quando usá-las pode ser confuso: embora a versatilidade das fatias faça delas uma escolha mais adequada na maioria das situações, existem instâncias específicas em que usar matrizes pode otimizar o desempenho do seu programa.

Este artigo tratará das matrizes e fatias em detalhes e, com isso, você terá as informações necessárias para fazer a escolha adequada quando precisar escolher entre esses tipos de dados. Além disso, você irá revisar as maneiras mais comuns de declarar e trabalhar tanto com matrizes como com fatias. O tutorial irá primeiro fornecer uma descrição das matrizes e como manipulá-las, seguida de uma explicação sobre as fatias e como elas diferem entre si.

Matrizes

As matrizes são estruturas de coleta de dados com um número fixo de elementos. Uma vez que o tamanho de uma matriz é estático, a estrutura de dados precisa atribuir a memória apenas uma vez, contrapondo-se a uma estrutura de dados de comprimento variável que tenha que atribuir a memória dinamicamente para que possa se tornar maior ou menor no futuro. Embora o comprimento fixo das matrizes possa torná-las um pouco rígidas para serem usadas, a atribuição de memória uma única vez pode aumentar a velocidade e o desempenho do seu programa. Por isso, os desenvolvedores normalmente usam matrizes ao otimizarem programas em instâncias nas quais a estrutura de dados nunca precisará de uma quantidade variável de elementos.

Definindo uma matriz

As matrizes são definidas pela declaração do tamanho da matriz entre parênteses [ ], seguida do tipo de dados dos elementos. Uma matriz em Go necessita que todos os seus elementos sejam do mesmo tipo de dados. Após o tipo de dados, é possível declarar os valores individuais dos elementos da matriz entre chaves { }.

A seguir, apresentamos o esquema geral para declarar uma matriz:

[capacity]data_type{element_values}

Nota: é importante lembrar que cada declaração de uma nova matriz cria um tipo distinto. Assim, embora [2]int e [3]int ambos tenham elementos inteiros, seus comprimentos diferentes tornam seus tipos de dados incompatíveis.

Se você não declarar os valores dos elementos da matriz, o padrão é o valor zero, o que significa que os elementos da matriz estarão vazios. Com os inteiros, isso é representado por 0 e, com as strings, isso é representado por uma string vazia.

Por exemplo, a matriz numbers, a seguir, possui três elementos inteiros que ainda não têm um valor:

var numbers [3]int

Se você imprimisse numbers, receberia o seguinte resultado:

Output
[0 0 0]

Se quisesse atribuir os valores dos elementos ao criar a matriz, colocaria os valores entre chaves. Uma matriz de strings com valores definidos teria a seguinte aparência:

[4]string{"blue coral", "staghorn coral", "pillar coral", "elkhorn coral"}

Você pode armazenar uma matriz em uma variável e imprimi-la:

coral := [4]string{"blue coral", "staghorn coral", "pillar coral", "elkhorn coral"}
fmt.Println(coral)

Executar um programa com as linhas acima daria a você o seguinte resultado:

Output
[blue coral staghorn coral pillar coral elkhorn coral]

Note que não há uma delineação entre os elementos na matriz quando ela é impressa, o que dificulta na hora de distinguir onde um elemento termina e o outro começa. Por conta disso, às vezes, pode ser útil usar a função fmt.Printf como alternativa, uma vez que ela consegue formatar as strings antes de imprimi-las na tela. Forneça o verbo %q com este comando para instruir a função a colocar sinais de aspas em volta dos valores:

fmt.Printf("%qn", coral)

Isso dará como resultado o seguinte:

Output
["blue coral" "staghorn coral" "pillar coral" "elkhorn coral"]

Agora, cada item está devidamente citado entre aspas. O verbo n instrui o formatador a adicionar um retorno de linha ao final.

Agora que você já tem uma ideia geral de como declarar matrizes e em que elas consistem, pode continuar aprendendo sobre como especificar elementos em uma matriz com um número de índice.

Indexando matrizes (e fatias)

Cada elemento em uma matriz (e também uma fatia) pode ser chamado individualmente através da indexação. Cada elemento corresponde a um número de índice, que é um valor int – que se inicia no número de índice 0 e segue em contagem ascendente.

Usaremos uma matriz nos exemplos a seguir, mas você poderia usar uma fatia também, já que elas são idênticas na forma como você indexa ambas.

Para a matriz coral, o desmembramento do índice se parece com este:

“blue coral” “staghorn coral” “pillar coral” “elkhorn coral”
0 1 2 3

O primeiro elemento, a string "blue coral", começa no índice 0 e a fatia termina no índice 3, com o elemento "elkhorn coral".

Como cada elemento em uma fatia ou matriz tem um número de índice correspondente, podemos acessá-lo e manipulá-lo das mesmas formas que fazemos com outros tipos de dados sequenciais.

Agora, podemos chamar um elemento discreto da fatia, recorrendo ao seu número de índice:

fmt.Println(coral[1])
Output
staghorn coral

Os números de índice dessa fatia variam de 0-3, como mostrado na tabela anterior. Assim, para chamar qualquer um dos elementos individualmente, nos referiríamos aos números de índice desta forma:

coral[0] = "blue coral"
coral[1] = "staghorn coral"
coral[2] = "pillar coral"
coral[3] = "elkhorn coral"

Se chamarmos a matriz coral com um número de índice maior que 3, ele estará fora do alcance e não será válido:

fmt.Println(coral[18])
Output
panic: runtime error: index out of range

Ao indexar uma matriz ou fatia, sempre utilize um número positivo. Ao contrário do que ocorre com algumas linguagens que permitem a indexação retroativa, com um número negativo, fazer isso em Go resultará em um erro:

fmt.Println(coral[-1])
Output
invalid array index -1 (index must be non-negative)

Podemos concatenar os elementos string em uma matriz ou fatia com outras strings usando o operador +:

fmt.Println("Sammy loves " + coral[0])
Output
Sammy loves blue coral

Conseguimos concatenar o elemento da string no número de índice 0 com a string "Sammy loves".

Com números de índice que correspondem a elementos dentro de uma matriz ou fatia, podemos acessar cada um dos elementos discretamente e trabalhar com esses elementos. Para demonstrar isso, vamos ver como modificar um elemento em um certo índice.

Modificando elementos

Podemos usar a indexação para alterar elementos dentro de uma matriz ou fatia, definindo um elemento numerado do índice com um valor diferente. Isso propicia maior controle sobre os dados em nossas fatias e matrizes e nos permitirá manipular elementos individuais por meio da programação.

Se quisermos alterar o valor da string do elemento no índice 1 da matriz coral de "staghorn coral“ para "foliose coral", isso pode ser feito da seguinte maneira:

coral[1] = "foliose coral"

Agora, quando imprimimos coral, a matriz será diferente:

fmt.Printf("%qn", coral)
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral"]

Agora que você sabe como manipular elementos individuais de uma matriz ou fatia, vamos examinar algumas funções que irão proporcionar mais flexibilidade ao trabalhar com tipos de dados de coleta.

Contando elementos com o len()

Na linguagem Go, len() é uma função integrada, criada para ajudar você a trabalhar com matrizes e fatias. Assim como com as strings, é possível calcular o comprimento de uma matriz ou fatia usando len() e transmitindo a matriz ou fatia como um parâmetro.

Por exemplo, para encontrar quantos elementos estão na matriz coral, você usaria:

len(coral)

Se imprimir o comprimento para a matriz coral, receberá o seguinte resultado:

Output
4

Isso dá o comprimento da matriz 4 no tipo de dados int, que é correto porque a matriz coral tem quatro itens:

coral := [4]string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral"}

Se você criar uma matriz de números inteiros com mais elementos, seria possível usar a função len() para isso também:

numbers := [13]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
fmt.Println(len(numbers))

Isso resultaria no seguinte:

Output
13

Embora essas matrizes de exemplo tenham relativamente poucos itens, a função len() é especialmente útil ao determinar quantos elementos estão em matrizes muito grandes.

Em seguida, vamos ver como adicionar um elemento a um tipo de dados de coleta. Demonstraremos também como e por que o comprimento fixo das matrizes gera erros ao acrescentarmos esses tipos de dados estáticos.

Acrescentando elementos com o append()

O append() é um método integrado em Go que adiciona elementos a um tipo de dados de coleta. No entanto, esse método não funcionará quando usado com uma matriz. Como mencionado anteriormente, a principal diferença entre matrizes e fatias é que o tamanho de uma matriz não pode ser modificado. Isso significa que, apesar de ser possível alterar os valores dos elementos em uma matriz, não é possível tornar a matriz maior ou menor após ter sido definida.

Vamos considerar sua matriz coral:

coral := [4]string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral"}

Digamos que você queira adicionar o item "black coral" a essa matriz. Se você tentar usar a função append() com a matriz, digitando:

coral = append(coral, "black coral")

Você receberá um erro como seu resultado:

Output
first argument to append must be slice; have [4]string

Para corrigir isso, vamos aprender mais sobre o tipo de dados de fatias, como definir uma fatia e como converter de uma matriz para uma fatia.

Fatias

Uma fatia é um tipo de dado em Go que é uma sequência ordenada de elementos mutável, ou modificável. Uma vez que o tamanho de uma fatia é variável, há muito mais flexibilidade ao usá-las; ao trabalhar com coleções de dados que possam precisar expandir ou contrair no futuro, usar uma fatia irá garantir que o seu código não apresente erros ao tentar manipular o comprimento da coleção. Na maioria dos casos, essa mutabilidade vale a possível realocação de memória – por vezes necessária – feita pelas fatias, em comparação com o que ocorre com as matrizes. Quando você precisar armazenar muitos elementos ou iterar sobre elementos e, além disso, se quiser ter a possibilidade de modificar tais elementos prontamente, é bem provável que você opte por trabalhar com o tipo de dados de fatias.

Definindo uma fatia

As fatias são definidas pela declaração do tipo de dados antecedido de um conjunto vazio de colchetes ([]) e uma lista de elementos entre chaves ({}). Você verá que, ao contrário do que ocorre com as matrizes que exigem um int entre parênteses para declarar um comprimento específico, uma fatia não tem nada entre parênteses, o que representa o seu comprimento variável.

Vamos criar uma fatia que contenha elementos do tipo de dados de string:

seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp", "anemone"}

Ao imprimirmos a fatia, podemos ver os elementos que estão na fatia:

fmt.Printf("%qn", seaCreatures)

Isso dará como resultado o seguinte:

Output
["shark" "cuttlefish" "squid" "mantis shrimp" "anemone"]

Se quiser criar uma fatia de um certo comprimento sem ainda preencher os elementos da coleção, utilize a função integrada make():

oceans := make([]string, 3)

Se imprimisse essa fatia, você teria:

Output
["" "" ""]

Se quiser fazer uma alocação prévia de memória para obter uma certa capacidade, você pode transmitir um terceiro argumento para make():

oceans := make([]string, 3, 5)

Isso criaria uma fatia zerada, com um comprimento de 3 e uma capacidade pré-alocada de 5 elementos.

Agora, você sabe como declarar uma fatia. No entanto, isso ainda não resolve o erro que tivemos anteriormente com a matriz coral. Para usar a função append() com coral, será primeiro necessário aprender como retirar seções de uma matriz.

Cortando matrizes em fatias

Ao usar números de índice para determinar pontos de início e de extremidade, você pode chamar uma subseção de valores dentro de uma matriz. Esse procedimento é conhecido como fatiar a matriz. Você pode fazer isso criando uma gama de números de índice, separados por dois pontos, na forma de [first_index:second_index]. No entanto, é importante notar que, ao fatiar uma matriz, o resultado é uma fatia, não uma matriz.

Vamos supor que você queira imprimir apenas os itens intermediários da matriz coral, sem o primeiro e último elemento. É possível fazer isso criando uma fatia que se inicia no índice 1 e termina imediatamente antes do índice 3:

fmt.Println(coral[1:3])

Executar um programa com essa linha resultaria no seguinte:

Output
[foliose coral pillar coral]

Ao criar uma fatia, como em [1:3], o primeiro número é onde a fatia inicia (inclusivo) e o segundo número é a soma do primeiro número e o número total de elementos que você gostaria de recuperar:

array[starting_index : (starting_index + length_of_slice)]

Neste exemplo, você chamou o segundo elemento (ou índice 1) de ponto de partida e chamou dois elementos no total. O cálculo ficaria parecido com este:

array[1 : (1 + 2)]

Que é como você chegou a esta notação:

coral[1:3]

Se quiser definir o início ou final da matriz como um ponto de partida ou final da fatia, você pode omitir um dos números na sintaxe array[first_index:second_index]. Por exemplo, se quiser imprimir os três primeiros itens da matriz coral — que seriam "blue coral", "foliose coral" e "pillar coral" — digite:

fmt.Println(coral[:3])

Isso imprimirá:

Output
[blue coral foliose coral pillar coral]

Esse procedimento imprimiu o início da matriz, parando logo antes do índice 3.

Para incluir todos os itens ao final de uma matriz, você inverteria a sintaxe:

fmt.Println(coral[1:])

O resultado seria a seguinte fatia:

Output
[foliose coral pillar coral elkhorn coral]

Nesta seção, discutimos sobre a chamada de partes individuais de uma matriz através do fatiamento em subseções. Em seguida, você aprenderá como usar o fatiamento para converter matrizes inteiras em fatias.

Convertendo uma matriz em uma fatia

Caso crie uma matriz e decida que ela precisa ter um comprimento variável, é possível convertê-la em uma fatia. Para converter uma matriz em uma fatia, utilize o processo de fatiamento que você aprendeu no passo Cortando matrizes em fatias deste tutorial. A exceção, desta vez, é que você irá selecionar a fatia inteira do índice, omitindo ambos os números de índice que definiriam os pontos de extremidade:

coral[:]

Lembre-se de que você não pode converter a variável coral em uma fatia em si, uma vez que, assim que uma variável é definida em Go, seu tipo não pode ser alterado. Para contornar esse problema, você pode copiar o conteúdo inteiro da matriz para uma nova variável como uma fatia:

coralSlice := coral[:]

Se imprimisse coralSlice, receberia o seguinte resultado:

Output
[blue coral foliose coral pillar coral elkhorn coral]

Agora, tente adicionar o elemento black coral de maneira parecida à seção da matriz, usando o append() com a fatia recém-convertida:

coralSlice = append(coralSlice, "black coral")
fmt.Printf("%qn", coralSlice)

Isso dará como resultado a fatia com o elemento adicionado:

Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral"]

Também podemos adicionar mais de um elemento em uma única instrução append():

coralSlice = append(coralSlice, "antipathes", "leptopsammia")
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral" "antipathes" "leptopsammia"]

Para combinar duas fatias juntas, é possível utilizar o append(). Porém, você precisará expandir o segundo argumento a ser acrescentado, usando a sintaxe de expansão ...:

moreCoral := []string{"massive coral", "soft coral"}
coralSlice = append(coralSlice, moreCoral...)
Output
["blue coral" "foliose coral" "pillar coral" "elkhorn coral" "black coral" "antipathes" "leptopsammia" "massive coral" "soft coral"]

Agora que você aprendeu como acrescentar um elemento à sua fatia, vamos ver como remover um deles.

Removendo um elemento de uma fatia

Ao contrário do que ocorre com outras linguagens, a linguagem Go não fornece nenhuma função integrada para remover um elemento de uma fatia. Os itens precisam ser removidos de uma fatia por meio de fatiamento.

Para remover um elemento, você deve remover os itens que estiverem antes e depois daquele elemento e, na sequência, acrescentar as duas novas fatias sem o elemento que você queria remover.

Se i for o índice do elemento a ser removido, então o formato desse processo ficaria como o seguinte:

slice = append(slice[:i], slice[i+1:]...)

Vamos remover o item "elkhorn coral" do coralSlice. Este item está localizado na posição de índice 3.

coralSlice := []string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral", "black coral", "antipathes", "leptopsammia", "massive coral", "soft coral"}

coralSlice = append(coralSlice[:3], coralSlice[4:]...)

fmt.Printf("%qn", coralSlice)
Output
["blue coral" "foliose coral" "pillar coral" "black coral" "antipathes" "leptopsammia" "massive coral" "soft coral"]

Agora, o elemento na posição de índice 3, a string "elkhorn coral", já não está em nossa fatia coralSlice.

Também podemos excluir uma série de itens com a mesma abordagem. Vamos supor que quiséssemos remover, além do item "elkhorn coral", também "black coral" e "antipathes". Podemos usar um intervalo na expressão para conseguir isso:

coralSlice := []string{"blue coral", "foliose coral", "pillar coral", "elkhorn coral", "black coral", "antipathes", "leptopsammia", "massive coral", "soft coral"}

coralSlice = append(coralSlice[:3], coralSlice[6:]...)

fmt.Printf("%qn", coralSlice)

Este código irá eliminar o índice 3, 4 e 5 da fatia:

Output
["blue coral" "foliose coral" "pillar coral" "leptopsammia" "massive coral" "soft coral"]

Agora que você sabe como adicionar e remover elementos de uma fatia, vamos ver como medir a quantidade de dados que uma fatia pode reter a qualquer momento.

Medindo a capacidade de uma fatia com o cap()

Como as fatias têm um comprimento variável, o método len() não é a melhor opção para determinar o tamanho desse tipo de dados. Em vez disso, use a função cap() para descobrir a capacidade de uma fatia. Esse procedimento mostrará quantos elementos uma fatia pode conter – o que é determinado pela quantidade de memória já alocada à fatia.

Nota: como o comprimento e capacidade de uma matriz são sempre os mesmos, a função cap() não funcionará em matrizes.

Um uso comum para o cap() é criar uma fatia com um número de elementos predefinidos e, na sequência, preencher esses elementos de acordo com o programa. Isso evita possíveis alocações desnecessárias que poderiam ocorrer pelo uso do append() para adicionar elementos, ultrapassando a capacidade já atribuída.

Tomemos como exemplo o cenário em que queremos fazer uma lista de números, 0 a 3. Para tanto, podemos usar o append() em um loop, ou podemos primeiro pré-alocar a fatia e depois usar o cap() para fazer o loop e preencher os valores.

Primeiro, podemos ver usando o append():

numbers := []int{}
for i := 0; i < 4; i++ {
    numbers = append(numbers, i)
}
fmt.Println(numbers)
Output
[0 1 2 3]

Neste exemplo, criamos uma fatia e, depois, criamos um loop for que fez iteração por quatro vezes. Cada iteração acrescentou o valor atual da variável de loop i ao índice da fatia numbers. Entretanto, isso poderia levar a alocações desnecessárias de memória que poderiam deixar seu programa mais lento. Ao adicionar algo a uma fatia vazia, cada vez que você fizer uma chamada para acrescentar, o programa vai verificar a capacidade da fatia. Se o elemento adicionado faz com que a fatia exceda sua capacidade, o programa alocará memória adicional para dar conta disso. Isso cria sobrecargas adicionais para o seu programa, podendo resultar em uma execução mais lenta.

Agora, vamos preencher a fatia sem usar o append(), através da alocação prévia de determinado comprimento/capacidade:

numbers := make([]int, 4)
for i := 0; i < cap(numbers); i++ {
    numbers[i] = i
}

fmt.Println(numbers)

Output
[0 1 2 3]

Neste exemplo, usamos make() para criar uma fatia e fizemos com que ela pré-alocasse 4 elementos. Então, usamos a função cap() no loop para iterar através de cada elemento zerado, preenchendo cada um até que fosse atingida a capacidade previamente alocada. Em cada loop, colocamos o valor atual da variável i do loop no índice da fatia numbers.

Apesar das estratégias de append() e cap() serem equivalentes em termos de função, o exemplo de cap() evita quaisquer alocações adicionais de memória que teriam sido necessárias com o uso da função append().

Construindo fatias multidimensionais

Também é possível definir fatias que consistam em outras fatias – como os elementos, tendo cada lista confinada entre parênteses, dentro de parênteses maiores da fatia-mãe. Coleções de fatias como essas são chamadas de fatias multidimensionais. Elas podem ser consideradas como uma representação de coordenadas multidimensionais; por exemplo, uma coleção de cinco fatias, que possui seis elementos cada, representa uma grade bidimensional com comprimento horizontal de cinco e uma altura vertical de seis.

Vamos examinar a seguinte fatia multidimensional:

seaNames := [][]string{{"shark", "octopus", "squid", "mantis shrimp"}, {"Sammy", "Jesse", "Drew", "Jamie"}}

Para acessar um elemento dentro dessa fatia, precisaremos usar índices múltiplos, senda cada um referente a uma dimensão da constructo:

fmt.Println(seaNames[1][0])
fmt.Println(seaNames[0][0])

No código anterior, identificamos primeiro o elemento no índice 0 da fatia no índice 1, depois, indicamos o elemento no índice 0 da fatia no índice 0. Isso resultará no seguinte:

Output
Sammy shark

A seguir estão os valores de índice para o resto dos elementos individuais:

seaNames[0][0] = "shark"
seaNames[0][1] = "octopus"
seaNames[0][2] = "squid"
seaNames[0][3] = "mantis shrimp"

seaNames[1][0] = "Sammy"
seaNames[1][1] = "Jesse"
seaNames[1][2] = "Drew"
seaNames[1][3] = "Jamie"

Ao trabalhar com fatias multidimensionais, é importante ter em mente que será necessário referir-se a mais de um número de índice para acessar elementos específicos dentro da fatia aninhada relevante.

Conclusão

Neste tutorial, você aprendeu os fundamentos do trabalho com matrizes e fatias em Go. Você passou por vários exercícios para demonstrar como matrizes tem comprimento fixo, ao passo que as fatias são variáveis em comprimento. Além disso, descobriu como essa diferença afeta os usos situacionais dessas estruturas de dados.

Para continuar estudando sobre as estruturas de dados em Go, consulte nosso artigo Entendendo maps em Go, ou explore toda a série de artigos sobre Como programar em Go.

Source: DigitalOcean