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:
- L'espressione slice completa
s[low:high:max]limita la capacità amax-low. Senza capacità libera, il prossimoappenddeve allocare.first := base[:2:2]rende sicuro il primo esempio. - Copia esplicitamente quando uno slice sopravviverà alla chiamata o riceverà append in modo indipendente:
slices.Clone(prefix)oppureappend([]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.
| Funzione | Cosa fa |
|---|---|
Contains, Index | cerca un valore (ContainsFunc, IndexFunc accettano un predicato) |
Sort, SortFunc, SortStableFunc | ordina sul posto |
BinarySearch | cerca in uno slice ordinato |
Insert, Delete, DeleteFunc | inserisce o rimuove, restituisce il nuovo slice |
Compact | rimuove i duplicati consecutivi |
Equal, Compare | confronta elemento per elemento |
Clone, Reverse, Max, Min | quello che ti aspetti |
Collect, Sorted, Values, All | lavorano 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 assegnareappend(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 poiappend. 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 solos == nil. Usaslices.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 [].