Menu

Slice in Golang: append, make, len e cap, e il bug della condivisione

Gli slice sono il tipo lista di tutti i giorni in Go. Scopri make, append e la crescita, len e cap, come lo slicing condivide l'array sottostante (e il bug che ne deriva), copy, il pacchetto slices, gli slice 2D e la differenza tra nil e vuoto.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Uno slice in un esempio

Uno slice è una vista espandibile su un array. Lo crei con un letterale o con make, lo fai crescere con append e ne leggi la dimensione con len.

[]string non ha una lunghezza tra le parentesi. È questa la differenza rispetto a un tipo array come [4]string, la cui dimensione è fissa.

Cos'è uno slice

Un valore slice è una piccola intestazione con tre campi:

  • un puntatore a un elemento di un array sottostante
  • una lunghezza: quanti elementi lo slice può vedere
  • una capacità: quanti elementi esistono da quel puntatore fino alla fine dell'array sottostante

Copiare uno slice (assegnarlo, passarlo a una funzione) copia solo questa intestazione. Entrambe le copie puntano agli stessi elementi. Quasi tutte le sorprese con gli slice nascono dal dimenticarlo.

make: lunghezza e capacità

make([]T, len, cap) alloca un array sottostante e restituisce uno slice su di esso. La capacità è facoltativa e per default è uguale alla lunghezza.

Una confusione comune: make([]int, 5) seguito da cinque chiamate ad append ti dà dieci elementi, i primi cinque a zero. Usa make([]int, 0, 5) quando intendi usare append, oppure make([]int, 5) quando intendi assegnare per indice.

Preallocare la capacità quando conosci la dimensione finale evita crescite ripetute. È un'ottimizzazione, non un obbligo.

append e la crescita

append aggiunge elementi dopo la lunghezza attuale. Se la capacità basta, scrive sul posto. Altrimenti alloca un array più grande, ci copia i vecchi elementi e restituisce uno slice che punta al nuovo array.

La capacità circa raddoppia finché lo slice è piccolo e cresce di un fattore minore (verso 1,25x) quando supera i 256 elementi. I numeri esatti sono un dettaglio del runtime che è cambiato tra le versioni di Go, quindi non scrivere mai codice che ne dipenda.

Dato che append può restituire un array diverso, devi usarne il risultato:

append(s, 4)     // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct

Per aggiungere un intero slice si usa la sintassi di espansione: s = append(s, other...).

Lo slicing condivide l'array sottostante

s[low:high] crea una nuova intestazione di slice sullo stesso array, che copre gli indici da low fino a high escluso. Si può omettere uno dei due estremi. Nessun elemento viene copiato.

Output:

[10 99 30 40 50]
3 4

È questa condivisione che rende lo slicing economico. Ed è anche l'origine del bug più famoso degli slice.

Il bug di aliasing con append

Quando un sotto-slice ha capacità libera, un append su di esso scrive nell'array del genitore, sopra elementi che il genitore usa ancora.

Output:

[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]

a e b condividono la posizione dell'array dopo "cmd", quindi il secondo append ha sovrascritto il primo. Il codice sembra corretto e funziona ogni volta che la capacità è già piena, ed è per questo che il bug compare solo a volte.

Due soluzioni:

  1. L'espressione slice completa s[low:high:max] limita la capacità a max-low. Senza capacità libera, il prossimo append deve allocare. first := base[:2:2] rende sicuro il primo esempio.
  2. Copia esplicitamente quando uno slice sopravviverà alla chiamata o riceverà append in modo indipendente: slices.Clone(prefix) oppure append([]string(nil), prefix...).

Una trappola collegata: un piccolo slice di un array enorme tiene in vita l'intero array per il garbage collector. Se leggi un file da 100 MB e tieni data[:10], i 100 MB restano in memoria. Clona la parte che ti serve.

copy

La funzione integrata copy(dst, src) copia min(len(dst), len(src)) elementi e restituisce quel numero. Non fa mai crescere dst.

copy gestisce correttamente sorgente e destinazione sovrapposte, quindi copy(s[1:], s) sposta gli elementi in avanti senza corromperli.

Il pacchetto slices

Da Go 1.21 il pacchetto standard slices offre funzioni generiche per le operazioni che prima scrivevi a mano.

FunzioneCosa fa
Contains, Indexcerca un valore (ContainsFunc, IndexFunc accettano un predicato)
Sort, SortFunc, SortStableFuncordina sul posto
BinarySearchcerca in uno slice ordinato
Insert, Delete, DeleteFuncinserisce o rimuove, restituisce il nuovo slice
Compactrimuove i duplicati consecutivi
Equal, Compareconfronta elemento per elemento
Clone, Reverse, Max, Minquello che ti aspetti
Collect, Sorted, Values, Alllavorano con gli iteratori (Go 1.23)

Delete e Insert restituiscono uno slice che devi assegnare, come append. L'ordinamento ha una sua pagina: ordinamento.

Rimuovere elementi mentre scorri per indice fa saltare degli elementi. Usa invece slices.DeleteFunc:

s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens

Slice 2D

Uno slice di slice ti dà una griglia le cui righe possono avere lunghezze diverse. Ogni riga va allocata separatamente.

Se dimentichi il make interno ogni riga resta nil, e grid[1][2] = 7 va in panic con index out of range [2] with length 0.

Slice nil e slice vuoti

Uno slice nil si comporta come uno vuoto per len, cap, range e append, quindi preferisci var s []T come stato zero. Inizializza a []T{} solo quando la differenza conta, cioè soprattutto nell'output JSON (null o []). Verifica se è vuoto con len(s) == 0, non con s == nil.

Slice e funzioni

Una funzione che riceve uno slice può modificarne gli elementi, e il chiamante vede le modifiche. Non può cambiare la lunghezza del chiamante, perché ha ricevuto una copia dell'intestazione. Una funzione che fa append deve restituire il nuovo slice:

func addAll(s []int, vals ...int) []int {
	return append(s, vals...)
}

Ecco perché append, slices.Delete e slices.Insert restituiscono tutti uno slice.

Errori comuni

  • Ignorare il risultato di append. Non compila se lo scarti del tutto, ma assegnare append(s, x) a una variabile diversa da quella che continui a usare è un bug di logica.
  • Due append dalla stessa base. Possono condividere la memoria. Clona la base o usa un'espressione slice completa.
  • make([]T, n) e poi append. Lascia n zeri all'inizio.
  • Indice fuori intervallo. s[len(s)] va in panic. L'ultimo elemento è s[len(s)-1].
  • Confrontare slice con ==. Compila solo s == nil. Usa slices.Equal.

Domande frequenti

Qual è la differenza tra lunghezza e capacità di uno slice in Go?

len(s) è il numero di elementi che lo slice contiene in quel momento. cap(s) è il numero di elementi che entrano nell'array sottostante a partire dal primo elemento dello slice. append scrive nella capacità libera quando c'è, e alloca un nuovo array più grande quando non c'è.

Come si aggiunge un elemento a uno slice in Go?

Chiama la funzione integrata append e riassegna il risultato: s = append(s, x). Può aggiungere più valori (append(s, 1, 2, 3)) o un altro slice (append(s, other...)). Usa sempre il valore restituito, perché append può restituire uno slice che punta a un nuovo array.

Come rimuovo un elemento da uno slice in Go?

Usa slices.Delete(s, i, i+1) (Go 1.21), che sposta indietro gli elementi successivi e restituisce lo slice più corto. Da Go 1.22 azzera anche le posizioni liberate in coda. Se l'ordine non conta, scambia con l'ultimo elemento e tronca: s[i] = s[len(s)-1]; s = s[:len(s)-1].

Come verifico se uno slice contiene un valore in Go?

Usa slices.Contains(s, v) dal pacchetto standard slices (Go 1.21). slices.Index(s, v) restituisce la posizione oppure -1, e slices.ContainsFunc accetta un predicato. Per ricerche ripetute su molti dati, costruisci invece una mappa.

Qual è la differenza tra uno slice nil e uno slice vuoto in Go?

var s []int è nil; s := []int{} è vuoto ma non nil. Entrambi hanno lunghezza 0 ed entrambi funzionano con len, range e append. La differenza si vede nei controlli s == nil e in encoding/json, che codifica uno slice nil come null e uno slice vuoto come [].

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA