Aprendendo Go: Arrays vs. Slices
Depois de ver a maneira correta de inicializar estruturas de dados em Go, agora vamos os conceitos de manipulação de listas, assim como, a utilização dessas estruturas de dados.
Esse artigo foi baseado no documento, gratuito e disponível em Effective Go
Arrays
Arrays
são úteis quando pudermos planejar detalhadamente o uso de memória da implementação em questão, logo, ajudando a reduzir o desperdício de memória alocada. Como parte principal disso, os arrays atuam mais como um construtor de slices
, que falaremos logo abaixo.
As maiores diferenças de funcionamento entre arrays
em Go e C são:
arrays
são valores, uma vez atribuído a outro, levará consigo todos seus elementosSe você passar um array para uma função, ele vai receber a cópia desse array e não um ponteiro.
O tamanho de um array compõe um tipo particular. O tipo
[10]T
e[20]T
são tipos diferentes.
Copiar os dados do array pode ser muito útil, entretanto, vai certamente consumir mais recursos, caso queira ver algo mais eficiente como em C você pode passar um ponteiro para o array.
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)
Apesar de ser possível, não é assim que se espera fazer código em Go, para isso você deveria utilizar slices
.
Slices
Slices
se concentra em ser uma abstração de arrays
, como um abstração acima do que falamos antes é mais genérico, poderoso e de melhor ergonomia. Com exceção de itens com dimensão conhecida, por exemplo matrizes e suas transformações, a maioria das implementações ficam melhores desta maneira.
O slice
guarda referência para um array
que o fundamenta, dessa forma, caso você atríbua um slice
a outro, ambos vão estar se referindo ao mesmo array
. Se uma função recebe um slice
como argumento e o modifica, logo, essa mudança estará visível para quem chamou a função porque este contém o array
base para o slice
, assim como funciona a passagem de um ponteiro para um array
.
A função Read
do package os
por exemplo, aceita um slice como argumento ao invés de aceitar um ponteiro ou um contados, utilizando do tamanho do slice para definir a quantidade de dados a serem lidos. Aqui está um exemplo dessa implementação:
func (f *File) Read(buf []byte) (n int, err error)
O método retorna um número de bytes
lidos e um error
caso exista. Para realizar a leitura dos 32 bytes
do buffer
maior chamado buf
você precisará fazer um slice
do buffer
, como quem tira uma fatia de um bolo.
n, err := f.Read(buf[0:32])
Apesar de ser uma boa implementação, o tamanho de um slice
tem uma natureza mutável uma vez que ele está diretamente associado a um terceiro, logo, responderá a esses limites. Sua capacidade máxima é acessível através da função nativa cap
. Abaixo está um exemplo de uma função que incrementa dados para um slice
. Se o dado ultrapassar a capacidade, o slice
será realocado e o resultado será retonado. A função usará do fato que len
e cap
são permitidos a nil slice
retornando 0
.
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) {
newSlice := make([]byte, (l+len(data))*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
Se faz necessário retornar um slice
posteriormente, porque, apesar do Append
poder modificar os elementos, o slice
por si só (ponteiro, tamanho e capacidade) foi passado por valor. A ideia por trás da função Append
foi tão apreciada que acabou se tornando nativa.
Matrizes
Em Go arrays
e slices
possuem apenas uma dimensão, para criar algo semelhante a matrizes, faz-se necessário definir um array-of-arrays
ou slice-of-slices
type Transform [3][3]float64
type LinesOfText [][]byte
Considerando que os slices
são uma variável com tamanho, é possível ter diferentes slices
internos com diferentes tamanhos, isso se pode ser uma situação comum, como a implementação de LinesOfText
onde cada linha tem uma implementação independente.
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
As vezes pode ser necessário alocar um 2D slice
, por exemplo, essa situação pode surgir ao resolver problemas como leitura de linhas de pixels. Existem dois caminhos para resolver isso, um deles é alocando cada slice
de maneira independente, e o outro é alocar um único array e um ponteiro com slices
individuais os quais estão independentes. Se o slice
pode crescer ou diminuir, ele deve ser alocado independentemente para evitar a sobrescrita da próxima linha, caso contrário, pode ser mais eficiente construir o objeto com uma única alocação.
Para referência, aqui estão esboços dos dois métodos. Primeiro, uma linha de cada vez:
picture := make([][]uint8, YSize)
for i := range picture {
picture[i] = make([]uint8, XSize)
}
E agora com uma alocação dividida em linhas
picture := make([][]uint8, YSize)
pixels := make([]uint8, XSize*YSize)
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Subscribe to my newsletter
Read articles from Daniel Suhett directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Daniel Suhett
Daniel Suhett
"Enquanto viver, continue aprendendo a viver." Seneca