Na seção anterior, discutimos arrays em Go. No entanto, os arrays possuem limitações: uma vez declarados e inicializados, seu comprimento não pode ser alterado. Por isso, os arrays não são amplamente utilizados na programação cotidiana. Em contrapartida, os slices (fatias) são mais comuns e oferecem uma estrutura de dados muito mais flexível.
Pontos de Conhecimento:
Definir um slice
Inicializar um slice
Operações em slices: adicionar, excluir, modificar e pesquisar
Expandir um slice
Truncamento de slice
Slices multidimensionais
Este é um Laboratório Guiado, que fornece instruções passo a passo para ajudar você a aprender e praticar. Siga as instruções cuidadosamente para concluir cada etapa e ganhar experiência prática. Dados históricos mostram que este é um laboratório de nível iniciante com uma taxa de conclusão de 93%. Ele recebeu uma taxa de avaliação positiva de 96% dos alunos.
O que é um Slice
Os slices são semelhantes aos arrays; são recipientes que armazenam elementos do mesmo tipo de dado. No entanto, os arrays têm limitações: uma vez declarados e inicializados, seu tamanho é fixo. Embora os arrays tenham seus casos de uso, eles não são tão flexíveis. Portanto, os slices são usados com muito mais frequência no dia a dia da programação.
Em Go, os slices são implementados internamente usando arrays. Um slice é, essencialmente, um array dinâmico que pode mudar de tamanho. Podemos realizar operações como adicionar, excluir, modificar e buscar elementos em um slice, o que não é possível fazer diretamente com um array de tamanho fixo.
Definir um Slice
A sintaxe de inicialização de um slice é muito parecida com a de um array. A principal diferença é que o comprimento do elemento não precisa ser especificado. Observe o código a seguir:
// Declare an array with a length of 5
var a1 [5]byte
// Declare a slice
var s1 []byte
Um Slice é uma Referência para um Array
Se declararmos um array do tipo int, o valor zero para cada elemento do array será 0.
No entanto, se declararmos um slice, o valor zero para o slice será nil. Vamos criar um arquivo chamado slice.go para verificar isso:
touch ~/project/slice.go
Insira o seguinte código:
package main
import "fmt"
func main() {
var a [3]int
var s []int
fmt.Println(a[0] == 0) // true
fmt.Println(s == nil) // true
}
go run slice.go
Após executar o código, a seguinte saída será exibida:
true
true
Criamos um array a e um slice s. Comparamos o primeiro elemento do array a com zero e verificamos se o slice s é nil.
Como podemos ver, quando declaramos um slice, seu valor inicial é nil. Isso ocorre porque os slices não armazenam dados por si mesmos; eles apenas referenciam arrays. O slice aponta para uma estrutura de array subjacente.
Estrutura de Dados de um Slice
Um slice é um tipo de dado composto, também conhecido como estrutura (struct). É um tipo formado por campos de diferentes tipos. A estrutura interna de um slice consiste em três elementos: um ponteiro, um comprimento (length) e uma capacidade (capacity).
type slice struct {
elem *type
len int
cap int
}
Como mencionado anteriormente, a estrutura referencia o array subjacente. O ponteiro elem aponta para o primeiro elemento do array, e o type é o tipo do elemento do array referenciado.
len e cap representam o comprimento e a capacidade do slice, respectivamente. Você pode usar as funções len() e cap() para obter esses valores.
A imagem a seguir mostra que o slice referencia um array subjacente do tipo int, com comprimento 5 e capacidade 5:
Quando você define um novo slice, o ponteiro elem é inicializado com o valor zero (ou seja, nil). O conceito de ponteiros será introduzido em laboratórios subsequentes. Por enquanto, saiba que um ponteiro aponta para o endereço de memória de um valor. Na imagem acima, o ponteiro elem aponta para o endereço do primeiro elemento do array subjacente.
Operações em Slices: Adicionar, Excluir, Modificar e Pesquisar
Truncamento de Arrays ou Slices
Como a estrutura subjacente de um slice é um array, podemos extrair um intervalo específico do array para servir como referência para o slice. O segmento de código a seguir demonstra isso:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Declare an empty slice
var s1 []int
fmt.Println("Slice s1 is empty:", s1 == nil)
// Use array truncation to obtain the slice
s1 = a[1:5]
s2 := a[2:5]
s3 := a[:]
fmt.Println("Slice s1, s2, and s3 are empty:", s1 == nil, s2 == nil, s3 == nil)
fmt.Println("Elements in array a:", a)
fmt.Println("Elements in slice s1:", s1)
fmt.Println("Elements in slice s2:", s2)
fmt.Println("Elements in slice s3:", s3)
}
A saída é a seguinte:
Slice s1 is empty: true
Slice s1, s2, and s3 are empty: false false false
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
Elements in slice s1: [1 2 3 4]
Elements in slice s2: [2 3 4]
Elements in slice s3: [0 1 2 3 4 5 6 7 8 9]
Neste programa, primeiro declaramos e inicializamos o array a e, em seguida, usamos o truncamento (fatiamento) do array para atribuir uma parte dele ao slice vazio s1. Ao fazer isso, criamos um novo slice.
s1[1:5] representa a criação de um slice do array. O intervalo do slice vai do índice 1 do array a até o índice 5, excluindo o quinto elemento.
Nota: Em linguagens de programação, o primeiro elemento está no índice 0, não 1. Portanto, o segundo elemento do array tem índice 1.
Usamos o operador := para atribuir o array truncado diretamente ao slice s2. O mesmo se aplica a s3, mas como nenhum intervalo foi especificado, ele trunca todos os elementos do array.
A imagem abaixo representa o truncamento. Note que a parte verde do slice representa uma referência ao array azul. Em outras palavras, ambos compartilham o mesmo array subjacente, que é a.
A sintaxe de truncamento para slices é a seguinte:
[inicio:fim]
Tanto inicio quanto fim são argumentos opcionais. Quando queremos obter todos os elementos do array, podemos omitir ambos. Isso foi demonstrado em s3 := a[:] no programa anterior.
Se quisermos recuperar todos os elementos após um certo índice, podemos omitir o parâmetro fim. Por exemplo, a1[3:] recuperará todos os elementos a partir do índice 3.
Para recuperar todos os elementos antes de um certo índice, omitimos o parâmetro inicio. Por exemplo, a1[:4] extrairá todos os elementos do índice 0 ao índice 4, excluindo o elemento no índice 4.
Além de extrair um slice de um array, também podemos extrair um novo slice de um slice já existente. A operação é a mesma. Veja um exemplo simples:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Create the initial slice s1
var s1 []int
s1 = a[1:7]
fmt.Printf("Slice s1: %d\tLength: %d\tCapacity: %d\n", s1, len(s1), cap(s1))
// Extract a new slice s2 from the initial slice s1
s2 := s1[2:4]
fmt.Printf("Slice s1: %d\tLength: %d\tCapacity: %d\n", s2, len(s2), cap(s2))
}
Neste programa, obtivemos o slice s1 truncando o array a. O intervalo de s1 vai do índice 1 ao 7. Usando :=, extraímos um novo slice s2 de s1. Como o truncamento de s1 é contíguo, o novo slice s2 também será contíguo.
Notamos que a capacidade de um slice muda conforme o truncamos. As regras são as seguintes:
Se truncarmos um slice com capacidade c, o comprimento de s[i:j] será j-i, e a capacidade será c-i.
Para s1, o array subjacente é a, e a capacidade de a[1:7] é 9 (ou seja, 10-1).
Para s2, o array subjacente é o mesmo de s1. Como a parte truncada tinha capacidade 9 na etapa anterior, a capacidade de s1[2:4] torna-se 7 (ou seja, 9-2).
Modificações nos Valores do Slice Afetam Simultaneamente o Array Subjacente
Como o slice não armazena dados, mas apenas referencia um array, modificar o valor de um slice também alterará simultaneamente o valor do array subjacente. Vamos demonstrar isso:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[2:5]
s2 := a[:]
fmt.Println("Before Modification: ")
fmt.Println("Elements in array a: ", a)
fmt.Println("Elements in array a: ", s1)
fmt.Println("Elements in array a: ", s2)
// Modify the value at index 2 of the slice s1 to 23
s1[2] = 23
fmt.Println("After Modification: ")
fmt.Println("Elements in array a: ", a)
fmt.Println("Elements in array a: ", s1)
fmt.Println("Elements in array a: ", s2)
}
A saída é a seguinte:
Before Modification:
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
Elements in array a: [2 3 4]
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
After Modification:
Elements in array a: [0 1 2 3 23 5 6 7 8 9]
Elements in array a: [2 3 23]
Elements in array a: [0 1 2 3 23 5 6 7 8 9]
Neste programa, tanto o slice s1 quanto o s2 referenciam o array a. Quando o slice s1 modifica o valor no seu índice 2 para 23, os valores do array a e do slice s2 também são atualizados.
Podemos ver que o valor no índice 4 do array foi modificado para 23 (porque s1 := a[2:5] significa que s1[0] refere-se a a[2], s1[1] a a[3] e s1[2] a a[4]). Isso resulta na modificação do valor no índice 4 tanto do array a quanto do slice s2.
Isso pode causar bugs difíceis de depurar no desenvolvimento de programas. Portanto, na programação diária, devemos tentar evitar ao máximo que múltiplos slices referenciem o mesmo array subjacente de forma descontrolada.
Adicionando Elementos a um Slice
Nesta seção, apresentaremos a função append, que é usada para adicionar elementos a um slice. A sintaxe é a seguinte:
func append(slice []Type, elems ...Type) []Type
O primeiro argumento é o slice, e os argumentos restantes são os elementos a serem adicionados. O []Type no final indica que a função append retornará um novo slice com o mesmo tipo de dado do original.
O elems após ... representa que este é um parâmetro variádico, o que significa que um ou mais parâmetros podem ser inseridos.
Neste programa, primeiro criamos o slice s1 truncando o array a. Usamos o operador = para atribuir o valor de retorno de append de volta a s1. Quando adicionamos elementos usando append, a capacidade do slice pode mudar. A capacidade do slice será aumentada se o número de elementos exceder a capacidade atual.
As regras de expansão para slices são as seguintes:
Se o array subjacente do slice puder acomodar os novos elementos, a capacidade do slice não mudará.
Se o array subjacente não puder acomodar os novos elementos, o Go criará um novo array maior, copiará os valores do slice original para o novo array e, em seguida, adicionará o novo valor.
Nota: A expansão do slice nem sempre dobra o tamanho; depende do tamanho dos elementos, da quantidade de elementos sendo expandidos e do hardware do computador, entre outros fatores. Para mais informações, consulte a seção avançada sobre slices.
Excluindo Elementos em um Slice
O Go não fornece palavras-chave ou funções específicas para excluir elementos de um slice, mas podemos usar o truncamento de array para alcançar essa funcionalidade, ou até capacidades mais poderosas.
O código a seguir exclui o elemento no índice 5 do slice s e o atribui a um novo slice s1:
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Print the slice `s` before deleting
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Print the slices `s` and `s1` after deleting the element at index 5
fmt.Printf("%d\n%d\n", s, s1)
}
A chave para excluir um índice específico reside na instrução append. Ela anexa os elementos após o índice 6 aos elementos antes do índice 5.
Essa operação é equivalente a uma sobrescrita antecipada. Por exemplo, o elemento 5 é sobrescrito pelo elemento 6, o elemento 6 pelo 7, e assim por diante, até o elemento 9. Portanto, o valor do array subjacente neste ponto torna-se [0 1 2 3 4 6 7 8 9 9].
Neste momento, se verificarmos o comprimento e a capacidade de s1, veremos que o comprimento é 9, mas a capacidade é 10. Isso ocorre porque s1 é uma referência ao truncamento de s. Eles compartilham o mesmo array subjacente.
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Check the length and capacity of s1
fmt.Println(len(s1), cap(s1))
fmt.Printf("\n%d\n%d\n\n", s, s1)
// Modify the slice s
s[3] = 22
fmt.Printf("%d\n%d\n", s, s1)
}
Além de excluir elementos em posições específicas, também podemos excluir um intervalo de elementos de um slice usando truncamento. A operação é a mesma que excluir um elemento em um índice específico. Vamos fazer um pequeno exercício.
Crie um arquivo slice1.go. Crie um slice a e inicialize-o conforme abaixo. Em seguida, use o truncamento para criar outro slice s que não inclua elementos maiores que 3 ou menores que 7. Por fim, imprima o novo slice.
a := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
Saída Esperada:
[9 8 7 3 2 1 0]
Dica: Preste atenção ao índice inicial do slice (lembre-se, o índice começa em 0).
Requisitos: O arquivo slice1.go deve ser colocado no diretório ~/project.
Um slice possui dois atributos: len (comprimento) e cap (capacidade). len representa o número de elementos atualmente no slice, enquanto cap representa o número máximo de elementos que o slice pode conter antes de precisar ser realocado.
O que acontece quando o número de elementos adicionados a um slice excede sua capacidade? Vamos descobrir juntos:
Before Append in s1: [0 0 0] Length: 3 Capacity: 3
Before Append in s2: [0 0 0] Length: 3 Capacity: 5
After Append in s1: [0 0 0 12] Length: 4 Capacity: 6
After Append in s2: [0 0 0 22] Length: 4 Capacity: 5
Como podemos ver neste programa, quando adicionamos elementos a um slice e o número de elementos excede sua capacidade original, a capacidade do slice é aumentada automaticamente.
As regras para expandir um slice são:
Se o array subjacente do slice puder conter os novos elementos, a capacidade não muda.
Se o array subjacente não puder conter os novos elementos, o Go criará um array maior, copiará os valores do slice original para o novo e adicionará os novos valores.
Nota: O dobro da capacidade nem sempre é a regra; depende do tamanho dos elementos, da quantidade de elementos e do hardware. Para mais detalhes, consulte materiais avançados sobre slices.
Copiando Slices
Podemos usar a função copy para duplicar um slice em outro. A sintaxe é a seguinte:
func copy(dst, src []Type) int
dst é o slice de destino, src é o slice de origem, e o int retornado indica o número de elementos copiados, que será o valor mínimo entre len(dst) e len(src).
Nota: A função copy não adiciona novos elementos (não aumenta o tamanho do destino).
Neste programa, copiamos os valores do slice s1 para o slice s2, e os valores de s4 para s3. A função copy retorna o número de elementos copiados.
Notamos que a primeira função copy tenta copiar s1[0, 1, 2, 3] para s2[8, 9]. Como o comprimento mínimo entre s1 e s2 é 2, ela copia apenas 2 valores. O slice de destino s2 é modificado para [0, 1].
A segunda função copy copia s4[8, 9] para s3[0, 1, 2, 3]. Como o comprimento mínimo entre s3 e s4 é 2, ela copia 2 valores. Como resultado, s3 é modificado para [8, 9, 2, 3].
Percorrendo Slices
Percorrer um slice é semelhante a percorrer um array. Todos os métodos de iteração de array também podem ser usados para slices.
Exercício
Neste exercício, testaremos e reforçaremos nossa compreensão sobre a iteração em slices e arrays.
Crie um arquivo slice2.go. Declare um array a1 e um slice s1, e inicialize-os conforme abaixo. Em seguida, itere sobre os elementos no array a1 e no slice s1, imprimindo seus índices e valores.
Element a1 at index 0 is 1
Element a1 at index 1 is 2
Element a1 at index 2 is 3
Element a1 at index 3 is 9
Element a1 at index 4 is 7
Element s1 at index 0 is 1
Element s1 at index 1 is 8
Element s1 at index 2 is 12
Element s1 at index 3 is 1
Element s1 at index 4 is 3
Requisitos: O arquivo slice2.go deve ser colocado no diretório ~/project.
Dica: Você pode usar o formato range ou o formato de índice para iterar pelos elementos.
Nesta seção, aprendemos sobre os slices e seu uso. Comparativamente, os slices são muito mais flexíveis e versáteis do que os arrays. Ao trabalhar com múltiplos slices, precisamos ser cautelosos para evitar operações inesperadas que afetem o array subjacente compartilhado.