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.