Exercício – Explorar fatias

Concluído

Na seção anterior, exploramos as matrizes e aprendemos que as matrizes são a base de fatias e mapas. Você entenderá o porquê disso daqui a pouco. Assim como as matrizes, a fatia é um tipo de dado no Go usado para representar uma sequência de elementos que têm o mesmo tipo. Mas a diferença mais significativa com as matrizes é que o tamanho de uma fatia é dinâmico, não fixo.

A fatia é uma estrutura de dados no alto de uma matriz ou de outra fatia. A matriz ou a fatia de origem é chamada de matriz subjacente. Com uma fatia, você pode acessar a matriz subjacente inteira ou apenas uma subsequência de elementos.

Uma fatia só tem três componentes:

  • Ponteiro para o primeiro elemento acessível da matriz subjacente. Este elemento não é necessariamente o primeiro elemento da matriz, array[0].
  • Comprimento da fatia. Número de elementos na fatia.
  • Capacidade da fatia. Número de elementos entre o início da fatia e o fim da matriz subjacente.

A seguinte imagem representa o que é uma fatia:

Diagram showing how slices look in Go.

Observe como a fatia é apenas um subconjunto da matriz subjacente. Vamos ver como você pode representar a imagem anterior no código.

Declarar e inicializar uma fatia

Para declarar uma fatia, faça isso do mesmo modo como declara uma matriz. Por exemplo, o seguinte código representa o que você viu na imagem da fatia:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    fmt.Println(months)
    fmt.Println("Length:", len(months))
    fmt.Println("Capacity:", cap(months))
}

Ao executar o código, você verá a seguinte saída:

[January February March April May June July August September October November December]
Length: 12
Capacity: 12

Observe como, no momento, uma fatia não é muito diferente de uma matriz. Você as declara do mesmo modo. Para obter as informações de uma fatia, use as funções internas len() e cap(). Continuaremos usando essas funções para confirmar se uma fatia tem uma subsequência de elementos de uma matriz subjacente.

Itens de uma fatia

O Go apresenta suporte para o operador de fatia s[i:p], em que:

  • s representa a matriz.
  • i representa o ponteiro para o primeiro elemento da matriz subjacente (ou outra fatia) a ser adicionado à nova fatia. A variável i corresponde ao elemento no local do índice i na matriz array[i]. Lembre-se de que este elemento não é necessariamente o primeiro elemento da matriz subjacente array[0].
  • p representa o número de elementos na matriz subjacente a serem usados quando a fatia for criada, além da posição do elemento. A variável p corresponde ao último elemento na matriz subjacente que pode ser usada na nova fatia. O elemento na posição p na matriz subjacente é encontrado no local array[i+1]. Observe que esse elemento não é necessariamente o último elemento da matriz subjacente, array[len(array)-1].

Portanto, uma fatia pode se referir apenas a um subconjunto de elementos.

Digamos que você deseja que quatro variáveis representem cada trimestre do ano e tenha uma fatia de months com 12 elementos. A imagem a seguir ilustra como fatiar months em quatro novas quarter fatias:

Diagram showing how multiple slices look in Go.

Para representar no código o que você viu na imagem anterior, use o seguinte código:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter1 := months[0:3]
    quarter2 := months[3:6]
    quarter3 := months[6:9]
    quarter4 := months[9:12]
    fmt.Println(quarter1, len(quarter1), cap(quarter1))
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter3, len(quarter3), cap(quarter3))
    fmt.Println(quarter4, len(quarter4), cap(quarter4))
}

Ao executar o código, você obterá a seguinte saída:

[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

Observe como o comprimento das fatias é o mesmo, mas a capacidade é diferente. Vamos explorar a fatia quarter2. Ao declarar essa fatia, você está dizendo que deseja iniciar a fatia na posição número três e o último elemento está localizado na posição número seis. O comprimento da fatia é de três elementos, mas a capacidade é nove, porque a matriz subjacente tem mais elementos ou posições disponíveis, mas não visíveis para a fatia. Por exemplo, se você tentar imprimir algo como fmt.Println(quarter2[3]), receberá o seguinte erro: panic: runtime error: index out of range [3] with length 3.

A capacidade de uma fatia apenas indica o quanto você pode estendê-la. Por esse motivo, você poderá criar uma fatia estendida de quarter2, como neste exemplo:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter2 := months[3:6]
    quarter2Extended := quarter2[:4]
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}

Ao executar o código anterior, você obterá a seguinte saída:

[April May June] 3 9
[April May June July] 4 9

Observe que, ao declarar a variável quarter2Extended, você não precisa especificar a posição inicial ([:4]). Quando você fizer isso, o Go presumirá que você deseja obter a primeira posição da fatia. Faça o mesmo para a última posição ([1:]). O Go presumirá que você deseja referenciar todos os elementos até a última posição de uma fatia (len()-1).

Acrescentar itens

Exploramos como as fatias funcionam e como elas são semelhantes às matrizes. Agora vamos descobrir como elas diferem das matrizes. A primeira diferença é que o tamanho de uma fatia não é fixo, é dinâmico. Depois de criar uma fatia, você pode adicionar mais elementos a ela, e a fatia será estendida. Daqui a pouco, você verá o que acontece com a matriz subjacente.

Para adicionar um elemento a uma fatia, o Go oferece a função interna append(slice, element). Você passa a fatia a ser modificada e o elemento a ser acrescentado como valores para a função. A função append retorna uma nova fatia que você armazena em uma variável. Pode ser a mesma variável para a fatia que você está alterando.

Vamos ver como é o processo de acréscimo dentro do código:

package main

import "fmt"

func main() {
    var numbers []int
    for i := 0; i < 10; i++ {
        numbers = append(numbers, i)
        fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
    }
}

Ao executar o código anterior, você verá a seguinte saída:

0       cap=1   [0]
1       cap=2   [0 1]
2       cap=4   [0 1 2]
3       cap=4   [0 1 2 3]
4       cap=8   [0 1 2 3 4]
5       cap=8   [0 1 2 3 4 5]
6       cap=8   [0 1 2 3 4 5 6]
7       cap=8   [0 1 2 3 4 5 6 7]
8       cap=16  [0 1 2 3 4 5 6 7 8]
9       cap=16  [0 1 2 3 4 5 6 7 8 9]

Esta saída é interessante. Especialmente para o que a chamada à função cap() está retornando. Tudo parece normal até a terceira iteração, em que a capacidade muda para 4, e há apenas três elementos na fatia. Na quinta iteração, a capacidade varia novamente para 8 e, na nona, para 16.

Você percebe um padrão na saída de capacidade? Quando uma fatia não tem capacidade suficiente para conter mais elementos, o Go duplica a capacidade dela. Isso cria uma nova matriz subjacente com a nova capacidade. Você não precisa fazer nada para que esse aumento na capacidade ocorra. O Go faz isso automaticamente. Tome cuidado. Em algum momento, a fatia pode estar com uma capacidade bem maior do que a necessária e você vai desperdiçar memória.

Remover itens

Você deve estar se perguntando: "E quanto à remoção de elementos?". Bem, o Go não tem uma função interna para remover elementos de uma fatia. Use o operador de fatia s[i:p] que mencionamos antes para criar uma fatia só com os elementos de que precisa.

Por exemplo, o seguinte código remove um elemento de uma fatia:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    remove := 2

	if remove < len(letters) {

		fmt.Println("Before", letters, "Remove ", letters[remove])

		letters = append(letters[:remove], letters[remove+1:]...)

		fmt.Println("After", letters)
	}

}

Ao executar o código anterior, você obterá a seguinte saída:

Before [A B C D E] Remove  C
After [A B D E]

Este código remove um elemento da fatia. Ele substituirá o elemento a ser removido pelo próximo elemento da fatia ou por nenhum se você estiver removendo o último elemento.

Outra abordagem é criar uma nova cópia da fatia. Aprenderemos como fazer cópias de fatias na próxima seção.

Criar cópias de fatias

O Go tem uma função interna copy(dst, src []Type) usada para criar cópias de uma fatia. Você envia a fatia de destino e a fatia de origem. Por exemplo, você pode criar uma cópia de uma fatia como mostra este exemplo:

slice2 := make([]string, 3)
copy(slice2, letters[1:4])

Por que a criação de cópias é importante? Bem, ao alterar um elemento de uma fatia, você também está alterando a matriz subjacente. Todas as outras fatias que se referem à mesma matriz subjacente serão afetadas. Vamos ver este processo no código e depois corrigi-lo criando uma cópia de fatia.

Use o código a seguir para confirmar se uma fatia aponta para uma matriz e se todas as alterações feitas em uma fatia afetam a matriz subjacente.

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]
    slice2 := letters[1:4]

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

Ao executar o código anterior, você verá a seguinte saída:

Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]

Observe como a alteração que fizemos em slice1 afetou a matriz letters e slice2. Você pode ver na saída que a letra B foi substituída por Z e isso afeta todas as pessoas que apontam para a matriz letters.

Para corrigir esse problema, você precisará criar uma cópia da fatia, que, nos bastidores, gera uma nova matriz subjacente. Você poderia usar o seguinte código:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]

    slice2 := make([]string, 3)
    copy(slice2, letters[1:4])

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

Ao executar o código anterior, você verá a seguinte saída:

Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]

Observe como a alteração em slice1 afetou a matriz subjacente, mas ela não afetou a nova slice2.