Aprendendo Go: Arrays vs. Slices

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 elementos

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