Arrays
Um array é uma coleção ordenada e de tamanho fixo de elementos do mesmo tipo, armazenada em blocos contíguos de memória. Cada posição no array é chamada de elemento, e cada elemento é acessado pelo seu index — um offset inteiro com base zero a partir do início.
var temperatures [5]float64
temperatures[0] = 20.1
temperatures[1] = 22.5
temperatures[2] = 19.8
temperatures[3] = 23.4
temperatures[4] = 21.0
fmt.Println(temperatures[0]) // 20.1
fmt.Println(temperatures[4]) // 21.0
fmt.Println(len(temperatures)) // 5
O built-in len retorna o número de elementos em um array. Como o comprimento é fixo, len sempre retorna o mesmo valor para um determinado tipo de array.
Declarando um array
Go oferece várias formas para declarar e inicializar arrays.
Forma básica: todos os elementos são definidos com o zero value do tipo.
var counts [4]int // [0, 0, 0, 0]
Com elementos: inicializa com valores específicos. O número de valores deve corresponder ao comprimento declarado.
primes := [5]int{2, 3, 5, 7, 11}
Comprimento implícito: ... instrui o compilador a contar os valores e definir o comprimento automaticamente.
primes := [...]int{2, 3, 5, 7, 11} // comprimento 5, inferido
Inicialização sparse: apenas certos índices são especificados — o restante recebe o zero value.
sparse := [5]int{2: 10, 4: 20} // [0, 0, 10, 0, 20]
Acessando elementos
Elementos são acessados com notação de colchetes. Os índices começam em zero, então o intervalo válido é de 0 a len(a) - 1. Acessar um index fora desse intervalo causa um runtime panic:
words := [3]string{"go", "is", "fast"}
fmt.Println(words[0]) // "go"
fmt.Println(words[2]) // "fast"
words[3] // panic: runtime error: index out of range [3] with length 3
Go não retorna silenciosamente um zero value nem faz wrap around — ele entra em panic imediatamente. Isso torna bugs de acesso fora dos limites fáceis de detectar.
Benefícios dos arrays
Como todos os elementos são armazenados de forma contígua na memória, acessar qualquer elemento por index é O(1) — o CPU calcula seu endereço diretamente a partir do endereço base e do index. Não há indireção, não há pointer chasing. Arrays são tão rápidos quanto o acesso à memória pode ser.
Usos comuns incluem buffers de tamanho fixo (pacotes de rede, headers de arquivo), lookup tables e como base para slices.
Limitações
Arrays em Go têm limitações rígidas que direcionam a maior parte do código real para slices.
Tamanho fixo. Uma vez declarado, o comprimento não pode mudar. Você não pode fazer append em um array nem remover elementos dele.
O tamanho faz parte do tipo. [3]int e [4]int são dois tipos distintos e incompatíveis. Uma função que aceita [3]int não pode receber um [4]int.
Sem comprimento variável. O comprimento deve ser uma constante conhecida em compile time — você não pode usar uma variável:
n := 5
var a [n]int // compile error: non-constant array bound n
Na maioria dos casos, slices são a escolha certa. Arrays são mais adequados para situações onde você conhece o tamanho exato em compile time e precisa de um layout de memória previsível.
Slices
Um slice é um wrapper leve em torno de um array. Ele oferece uma visão dinâmica sobre um underlying array: você pode crescê-lo, encolhê-lo e passá-lo adiante sem copiar os dados.
Um slice tem três componentes internamente:
- Um pointer para o início de sua janela no underlying array
- Um length — o número de elementos atualmente visíveis pelo slice
- Uma capacity — o número total de elementos disponíveis do pointer até o final do underlying array
s := []int{10, 20, 30, 40, 50}
fmt.Println(len(s)) // 5 — número de elementos no slice
fmt.Println(cap(s)) // 5 — tamanho do underlying array
Declarando um slice
Slices são declarados sem comprimento — é isso que os distingue de arrays sintaticamente.
Nil slice: O zero value de um slice é nil. Ele não tem underlying array, length 0 e capacity 0.
var s []int // nil slice
Empty slice: Tem um underlying array (alocado mas sem elementos armazenados), length 0 e capacity 0.
s := []int{} // empty slice
Com elementos:
s := []int{10, 20, 30}
nil vs empty slice
Um nil slice e um empty slice se comportam de forma idêntica em quase toda situação — len retorna 0, cap retorna 0, range itera zero vezes e append funciona em ambos. A diferença aparece em um lugar específico: encoding JSON.
var a []int // nil slice
b := []int{} // empty slice
fmt.Println(a == nil) // true
fmt.Println(b == nil) // false
fmt.Println(len(a) == len(b)) // true — ambos 0
json.Marshal codifica um nil slice como null e um empty slice como []. Para a maioria dos outros fins, prefira nil slices — eles são o zero value idiomático.
A função make
make cria um slice com um length especificado e uma capacity opcional. É a forma preferida de criar um slice quando você sabe o tamanho mas ainda não tem valores iniciais.
s := make([]int, 5) // len=5, cap=5
s := make([]int, 3, 10) // len=3, cap=10
Fornecer uma dica de capacity quando você sabe aproximadamente quantos elementos vai fazer append evita realocações desnecessárias depois.
A função append
append adiciona um ou mais elementos ao final de um slice e retorna o slice atualizado. Você deve atribuir o valor de retorno — se append precisar alocar um novo underlying array, a variável original continua apontando para o antigo.
s := []int{1, 2, 3}
s = append(s, 4) // [1, 2, 3, 4]
s = append(s, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]
Para unir dois slices, expanda o segundo com ...:
a := []int{1, 2, 3}
b := []int{4, 5, 6}
c := append(a, b...) // [1, 2, 3, 4, 5, 6]
Sempre atribua o valor de retorno do append
Se você chamar append sem atribuir o resultado, as alterações são silenciosamente descartadas. Esse é um erro comum que o compilador não detecta porque append é uma expressão válida por si só.
Por dentro: crescimento
Quando append precisa de mais capacity do que a disponível, Go aloca um novo underlying array maior, copia todos os elementos existentes para ele e retorna um slice apontando para o novo array. Isso é automático — você nunca gerencia isso manualmente.
A estratégia de crescimento em Go é aproximadamente:
- Para slices pequenos (abaixo de ~256 elementos): dobra a capacity
- Para slices maiores: cresce aproximadamente 25%
s := make([]int, 0, 2)
for i := 0; i < 6; i++ {
s = append(s, i)
fmt.Printf("len=%d cap=%d\n", len(s), cap(s))
}
// len=1 cap=2
// len=2 cap=2
// len=3 cap=4 ← novo array alocado, capacity dobrada
// len=4 cap=4
// len=5 cap=8 ← novo array alocado, capacity dobrada
// len=6 cap=8
Cada realocação é O(n) — mas como a capacity dobra a cada vez, o trabalho total em todos os appends é amortizado O(1) por elemento.
Indexação
Slices usam a mesma notação de colchetes que arrays e também são zero-indexed. O intervalo válido é de 0 a len(s) - 1. Acessar fora desse intervalo causa um runtime panic, assim como com arrays:
s := []int{10, 20, 30}
fmt.Println(s[0]) // 10
fmt.Println(s[2]) // 30
s[-1] // panic: index out of range [-1]
s[3] // panic: index out of range [3] with length 3
Slicing
Você pode criar um novo slice a partir de um slice ou array existente usando uma slice expression s[low:high]. O resultado é um novo slice header apontando para o mesmo underlying array — nenhum dado é copiado.
a := []int{10, 20, 30, 40, 50}
b := a[1:4] // [20, 30, 40]
O intervalo é half-open: inclui o elemento em low e exclui o elemento em high. a[1:4] cobre os índices 1, 2 e 3 — três elementos.
Qualquer bound pode ser omitido:
a[:3] // três primeiros elementos: [10, 20, 30]
a[2:] // do índice 2 até o final: [30, 40, 50]
a[:] // slice inteiro: [10, 20, 30, 40, 50]
A capacity do novo slice se estende de low até o final do underlying array:
a := []int{10, 20, 30, 40, 50}
b := a[1:3] // len=2, cap=4 (do índice 1 até o final do backing array de a)
O ripple effect
Como o slicing não copia dados, modificar elementos por meio de um slice modifica o underlying array — e qualquer outro slice que o compartilhe:
a := []int{10, 20, 30, 40, 50}
b := a[1:4]
b[0] = 99
fmt.Println(a) // [10, 99, 30, 40, 50] — a[1] foi alterado através de b
fmt.Println(b) // [99, 30, 40]
Isso também se aplica ao append quando há capacity restante: fazer append em b pode sobrescrever elementos de a que estão além do length atual de b.
append em um sub-slice pode sobrescrever o slice pai
Se b := a[1:3] e cap(b) > len(b), então append(b, x) escreve x em a[3] — a posição logo após o final de b no array compartilhado. Esse é um dos comportamentos mais surpreendentes dos slices em Go.
Evitando o ripple effect com copy
Quando você precisa de um slice independente que não compartilhe memória com a fonte, use copy:
src := []int{10, 20, 30, 40, 50}
dst := make([]int, len(src))
copy(dst, src)
dst[0] = 99
fmt.Println(src) // [10, 20, 30, 40, 50] — inalterado
fmt.Println(dst) // [99, 20, 30, 40, 50]
copy(dst, src) copia o mínimo entre len(dst) e len(src) elementos e retorna o número de elementos copiados. Os dois slices não compartilham nada após o copy.