Diferenças entre slice e array em Go

Array

Array em Go sempre tem tamanho fixo.

Quando você adiciona um novo elemento em um array o que acontece internamente é que um novo array é criado e os itens antigos e novos são copiados para esse novo array. Eventualmente o espaço opupado pelo array antigo será liberado pelo garbage collector. Então é importante, sempre que possível, declarar o array com o tamanho máximo que ele vai precisar e evitar adicionar itens, principalmente em loop.

Slice

Slice por outro lado são uma abstração para acessar o conteúdo de um array, você pode pensar no slice como uma estrutura de ponteiros que aponta para os itens de um array e sabe onde iniciar, tamanho do slice e a capacidade do array. Isso torna as operações com slices muito rápidas.

Isso pode causar alguma confusão porque se você tem um array que comporta digamos quatro elementos e você adicionar mais um com append via um slice você vai ter um novo array apontando pelo slice mas a referencia antiga ao array ainda vai funcionar.

package main

import (
    "fmt"
)

func main() {
    // este é um array
    a := [6]string{"isso", "é", "um", "coleção", "de", "palavras"}
    
    // aqui criamos um slice apontando para o array a
    s := a[:]
    
    fmt.Printf("valor de a[3]: %q\n", a[3])
    fmt.Printf("valor de s[3]: %q\n", s[3])
    
    // como s é um ponteiro para a, ao mudar
    // s também mudamos a
    fmt.Println(`— Como "s" aponta para "a", mudar "s" também muda "a"`)
    s[3] = "array"
    fmt.Printf("valor de a[3]: %q\n", a[3])
    fmt.Printf("valor de s[3]: %q\n", s[3])
    
    // vamos agora fazer um append no slice s
    fmt.Println(`— Fazer append em "s" cria um novo array`)
    s = append(s, "!")
    
    // isso criou um novo array e copiou os dados antigos
    // agora vamos mudar o valor de s[3] para demonstrar isso
    s[3] = "slice"
    fmt.Printf("valor de a[3]: %q\n", a[3])
    fmt.Printf("valor de s[3]: %q\n", s[3])
}

O exemplo anterior mostra o seguinte resultado:

valor de a[3]: "coleção"
valor de s[3]: "coleção"
— Como "s" aponta para "a", mudar "s" também muda "a"
valor de a[3]: "array"
valor de s[3]: "array"
— Fazer append em "s" cria um novo array
valor de a[3]: "array"
valor de s[3]: "slice"

Note que na primeira vez que s[3] é alterado a[3] também é alterado. Isso porque s está apontando para a. Depois que fazermos append em s para adicionar um novo ontem um novo array é criado e s passa a não apontar mais para a e sim para esse novo array. E como ainda existe uma referencia para a o garbage collector ainda não vai tocar nele.

Usar slices e pré alocar o array garante uma ótima velocidade, mas é necessário tomar cuidado para não se confundir, não compreender o funcionamento de slices e arrays é uma bela fonte de bugs.

Veja o seguinte exemplo:

package main

import "fmt"

func main() {
	a := [3]int{0, 0, 0}
	v := a[:]
	for i := 0; i <= 5; i++ {
		v = append(v, i)
	}
	fmt.Println(v)
}

Mais uma vez estamos usando um slice para acessar um array pré alocado, como estamos fazendo append sem se importar com o tamanho já alocado vamos acabar com um array com oito itens e não quatro.

Veja o resultado do exemplo:

[0 0 0 0 1 2 3 4]

O correto para esse exemplo seria verificar a capacidade do array antes de adicionar um novo item, assim você pode decidir se deve fazer apenas uma atribuição um append.

len() e cap()

Go tem duas funções internas que podem ser usadas para explorar um slice, len() que retorna o tamanho efetivamente usado pelo slice atual e cap() que retorna a capacidade atual do array apontado pelo slice.

Vamos modificar o exemplo anterior para acompanhar usar essas funções e investigarmos o comportamento do Go e o espaço alocado para o array.

package main

import "fmt"

func main() {
	a := [3]int{0, 0, 0}
	v := a[:]
	fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
	for i := 0; i <= 4; i++ {
		v = append(v, i)
		fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
	}
	fmt.Println(v)
}

O resultado desse exemplo vai ser o seguinte:

len: 3 cap: 3
len: 4 cap: 8
len: 5 cap: 8
len: 6 cap: 8
len: 7 cap: 8
len: 8 cap: 8
[0 0 0 0 1 2 3 4]

Note que como Go conseguiu prever o numero de interações do for ele foi esperto e pré-alocou o array já com os itens necessários, essa otimização ajuda muito, mas em uma situação em que o compilador não consiga prever a quantidade de interações a cada interação Go vai realocar o array todo. E claro, ainda estamos cometendo o erro de não usar os primeiros itens do nosso array porque estamos sempre fazendo append.

Vamos ver uma forma de melhorar isso:

package main

import "fmt"

func main() {
	a := [3]int{0, 0, 0}
	v := a[:]
	fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
	for i := 0; i <= 4; i++ {
		if i < len(v) {
			v[i] = i
			fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
			continue
		}
		v = append(v, i)
		fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
	}
	fmt.Println(v)
}

Nesse exemplo verificamos o tamanho do slice antes e decidimos se queremos fazer append ou não. Mas nesse caso Go tenta pré alocar mais espaço para possíveis novos appends e acaba desperdiçando memória.

Veja o resultado do exemplo:

len: 3 cap: 3
len: 3 cap: 3
len: 3 cap: 3
len: 3 cap: 3
len: 4 cap: 8
len: 5 cap: 8
[0 1 2 3 4]

Não estamos mais fazendo apenas append mais a capacidade do array é maior que a necessária.

A melhor maneira de resolver esse problema é pré-alocando o array com o tamanho que sabemos que será o necessário.

package main

import "fmt"

func main() {
	a := [5]int{}
	v := a[:]
	fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
	for i := 0; i <= 4; i++ {
		if i < len(v) {
			v[i] = i
			fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
			continue
		}
		v = append(v, i)
		fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
	}
	fmt.Println(v)
}

E o resultado do código anterior é esse:

len: 5 cap: 5
len: 5 cap: 5
len: 5 cap: 5
len: 5 cap: 5
len: 5 cap: 5
len: 5 cap: 5
[0 1 2 3 4]

Agora finalmente o resultado e a capacidade estão corretos e não fazemos mais append então podemos excluir esse trecho de código.

package main

import "fmt"

func main() {
	a := [5]int{}
	v := a[:]
	fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
	for i := 0; i <= 4; i++ {
		v[i] = i
		fmt.Printf("len: %v cap: %v\n", len(v), cap(v))
	}
	fmt.Println(v)
}

Agora que o nosso código não faz mais append ele é muito mais rápido e garbage collector friendly.

Cesar Gimenes

Última modificação