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:]
}