Slice e Mappe in Go
In Go, le slice e le mappe sono due strutture dati fondamentali per organizzare e manipolare informazioni. Le slice permettono di lavorare con sequenze di elementi di lunghezza variabile, mentre le mappe associano chiavi a valori e consentono di recuperare rapidamente un dato attraverso la sua chiave. Conoscere le differenze tra queste strutture è importante per scrivere codice chiaro, efficiente e corretto.
Le slice sono costruite sopra gli array e offrono un'interfaccia flessibile per aggiungere, modificare e leggere elementi. La funzione integrata append() consente di aggiungere nuovi valori, mentre make() permette di specificare la lunghezza iniziale e la capacità della slice. Comprendere la distinzione tra questi due concetti aiuta a evitare allocazioni inutili e comportamenti inattesi quando più slice condividono lo stesso array sottostante.
Le mappe, invece, rappresentano collezioni di coppie chiave-valore. Sono adatte, per esempio, a memorizzare configurazioni, contare occorrenze, indicizzare record e associare identificativi a oggetti. Il loro utilizzo richiede alcune precauzioni: una mappa deve essere inizializzata prima di inserirvi elementi e l'ordine di iterazione non è garantito.
In questa guida vedremo come dichiarare e inizializzare slice e mappe, come modificarle, come scorrerne gli elementi con range e quali dettagli del comportamento di Go è importante conoscere per evitare errori.
Slice
Una slice è una struttura che descrive una porzione di un array sottostante. Non è un array dinamico autonomo: contiene un riferimento all'array, una lunghezza e una capacità. La lunghezza, restituita da len(), indica quanti elementi sono attualmente accessibili nella slice; la capacità, restituita da cap(), indica quanti elementi possono essere contenuti a partire dal suo primo elemento prima di dover utilizzare un array sottostante più grande.
Possiamo dichiarare una slice senza inizializzarla e aggiungere successivamente gli elementi con append():
package main
import "fmt"
func main() {
var numeri []int
numeri = append(numeri, 1, 2, 3)
fmt.Println(numeri)
fmt.Println("Lunghezza:", len(numeri))
fmt.Println("Capacità:", cap(numeri))
}La slice iniziale è nil, ma append() restituisce una slice valida contenente i nuovi elementi. È importante assegnare il risultato alla variabile, perché l'operazione può restituire una slice con un array sottostante differente da quello precedente.
Un'altra possibilità consiste nell'utilizzare make() per inizializzare la slice con una capacità prevista:
numeri := make([]int, 0, 10)
numeri = append(numeri, 1, 2, 3)In questo caso la lunghezza iniziale è zero, mentre la capacità è dieci. La slice può quindi contenere fino a dieci elementi senza richiedere un array sottostante più grande, purché non vengano introdotte altre condizioni che cambino la situazione.
La capacità iniziale è utile quando si conosce approssimativamente il numero di elementi che verranno aggiunti. Può ridurre il numero di riallocazioni, ma non è necessario impostarla sempre: conviene scegliere un valore iniziale ragionevole sulla base delle esigenze effettive dell'applicazione.
Lunghezza e capacità
La differenza tra len() e cap() è fondamentale. Consideriamo questo esempio:
numeri := make([]int, 3, 10)
fmt.Println(len(numeri))
fmt.Println(cap(numeri))L'output è:
3
10La slice contiene già tre elementi, inizializzati al valore zero del tipo int, e dispone di una capacità pari a dieci. Non è però possibile accedere direttamente all'indice 3 finché la lunghezza non viene aumentata. Per aggiungere un elemento possiamo usare append():
numeri = append(numeri, 42)
fmt.Println(numeri)La nuova slice contiene quattro elementi. La capacità rimane almeno sufficiente per contenerli, ma il comportamento preciso della crescita della capacità è un dettaglio gestito dall'implementazione di Go.
Aggiungere e copiare elementi
La funzione append() consente di aggiungere uno o più elementi a una slice:
numeri := []int{1, 2, 3}
numeri = append(numeri, 4, 5)
fmt.Println(numeri)Il risultato è [1 2 3 4 5].
Per aggiungere tutti gli elementi di un'altra slice si utilizza l'operatore ...:
altriNumeri := []int{6, 7, 8}
numeri = append(numeri, altriNumeri...)L'operatore espande gli elementi della seconda slice come argomenti di append().
Per copiare gli elementi in una slice differente è disponibile la funzione integrata copy():
origine := []int{10, 20, 30}
destinazione := make([]int, len(origine))
copiati := copy(destinazione, origine)
fmt.Println(destinazione)
fmt.Println("Elementi copiati:", copiati)La funzione copia gli elementi fino alla lunghezza della slice più corta e restituisce il numero di elementi copiati. In questo esempio, la destinazione ha la stessa lunghezza dell'origine e riceve tutti e tre i valori.
Slice e array sottostante
Quando si lavora con le slice è importante capire che assegnare una slice a un'altra variabile non copia automaticamente tutti gli elementi. Viene copiato il valore della slice, che continua a riferirsi allo stesso array sottostante.
originale := []int{1, 2, 3}
altra := originale
altra[0] = 99
fmt.Println(originale)
fmt.Println(altra)Entrambe le stampe mostreranno [99 2 3], perché le due slice condividono lo stesso array sottostante.
Anche il passaggio di una slice a una funzione comporta la copia del valore della slice, non dell'intero array. La funzione può modificare gli elementi condivisi attraverso la slice ricevuta. Se invece deve rendere visibile al chiamante una nuova lunghezza ottenuta con append(), normalmente deve restituire la slice aggiornata.
func aggiungiElemento(numeri []int, valore int) []int {
numeri = append(numeri, valore)
return numeri
}L'assegnazione del risultato è importante perché append() può allocare un nuovo array sottostante. Il chiamante deve quindi acquisire la slice restituita per utilizzare correttamente la nuova lunghezza.
Mappe
Le mappe (map) associano chiavi a valori e sono utili quando occorre recuperare un elemento tramite un identificatore anziché attraverso una posizione numerica. Per esempio, una mappa può associare il nome di una proprietà al relativo valore, oppure un codice prodotto alla quantità disponibile.
Una mappa può essere dichiarata e poi inizializzata con make():
package main
import "fmt"
func main() {
var persona map[string]string
persona = make(map[string]string)
persona["nome"] = "Luca"
persona["cognome"] = "Rossi"
fmt.Println(persona["nome"])
}È importante ricordare che una mappa dichiarata senza inizializzazione ha valore nil. È possibile leggere da una mappa nil, ma non inserirvi nuove coppie chiave-valore: un'assegnazione provocherebbe un panic. Per questo motivo, prima di aggiungere elementi, occorre inizializzarla con make() oppure utilizzare un literal.
Quando i dati sono già noti, possiamo creare e popolare una mappa direttamente:
persona := map[string]string{
"nome": "Luca",
"cognome": "Rossi",
}Questa sintassi rende il codice più compatto e permette di vedere immediatamente quali chiavi sono presenti nella struttura iniziale.
Verificare la presenza di una chiave
Quando si legge un valore da una mappa, Go restituisce il valore associato alla chiave oppure il valore zero del tipo se la chiave non esiste. Per distinguere i due casi è possibile utilizzare il secondo valore restituito dall'indicizzazione:
persona := map[string]string{
"nome": "Luca",
}
nome, presente := persona["nome"]
fmt.Println(nome, presente)
cognome, presente := persona["cognome"]
fmt.Println(cognome, presente)Il secondo valore, di tipo bool, indica se la chiave è presente. Questo controllo è particolarmente importante quando il valore zero è un dato valido: per esempio, una mappa di conteggi potrebbe contenere il valore 0, che non permette da solo di stabilire se la chiave esista.
Aggiornare e rimuovere elementi
Le mappe consentono di aggiungere nuove coppie chiave-valore e aggiornare quelle già presenti attraverso la stessa sintassi:
quantita := map[string]int{
"penne": 10,
"matite": 5,
}
quantita["penne"] = 12
quantita["quaderni"] = 7
delete(quantita, "matite")L'assegnazione aggiorna il valore di penne, aggiunge quaderni e delete() rimuove la chiave matite, se presente. Eliminare una voce non modifica le altre coppie della mappa.
Le mappe sono particolarmente utili per contare occorrenze. Per esempio, possiamo contare quante volte compare ogni parola in una sequenza:
parole := []string{"go", "java", "go", "python", "go", "java"}
conteggi := make(map[string]int)
for _, parola := range parole {
conteggi[parola]++
}In Go, leggere il valore di una chiave assente restituisce il valore zero del tipo int. Perciò l'incremento funziona anche quando la parola non è ancora stata inserita: la prima occorrenza porta il conteggio da zero a uno.
Iterare con range
La costruzione range permette di scorrere gli elementi di una slice o di una mappa senza dover gestire manualmente gli indici o cercare le chiavi.
Per una slice, range restituisce l'indice e una copia del valore dell'elemento:
numeri := []int{10, 20, 30}
for indice, valore := range numeri {
fmt.Printf("Indice %d: %d\n", indice, valore)
}L'output è:
Indice 0: 10
Indice 1: 20
Indice 2: 30Se non serve l'indice, è possibile ignorarlo con _:
for _, valore := range numeri {
fmt.Println(valore)
}È importante ricordare che la variabile valore contiene una copia dell'elemento. Modificarla non cambia automaticamente il valore memorizzato nella slice:
for _, valore := range numeri {
valore *= 2
}In questo esempio, la slice originale rimane invariata. Per modificare gli elementi è necessario utilizzare gli indici:
for indice := range numeri {
numeri[indice] *= 2
}Quando si itera su una mappa, range restituisce invece la chiave e il valore associato:
persona := map[string]string{
"nome": "Luca",
"cognome": "Rossi",
}
for chiave, valore := range persona {
fmt.Printf("%s: %s\n", chiave, valore)
}L'ordine di iterazione delle mappe non è garantito. Non bisogna quindi fare affidamento sull'ordine con cui le chiavi vengono visitate. Se occorre produrre un risultato ordinato, è possibile raccogliere le chiavi in una slice e ordinarle con il pacchetto sort:
import "sort"
chiavi := make([]string, 0, len(persona))
for chiave := range persona {
chiavi = append(chiavi, chiave)
}
sort.Strings(chiavi)
for _, chiave := range chiavi {
fmt.Printf("%s: %s\n", chiave, persona[chiave])
}Questo approccio permette di ottenere un ordine deterministico in base all'ordinamento delle stringhe, utile per produrre report, output leggibili e risultati coerenti nei test.
Conclusione
Slice e mappe risolvono esigenze diverse ma complementari. Le slice sono adatte a gestire sequenze di elementi, supportano operazioni come append() e copy() e consentono di controllare la lunghezza e la capacità attraverso len() e cap(). Le mappe permettono invece di associare chiavi a valori, verificare la presenza di una chiave, aggiornare le informazioni e rimuovere elementi con delete().
La costruzione range semplifica l'iterazione su entrambe le strutture, ma è importante ricordare che i valori restituiti sono copie e che l'ordine di iterazione delle mappe non è garantito. Allo stesso modo, comprendere come le slice condividono l'array sottostante e come append() può restituire una slice diversa aiuta a evitare errori difficili da individuare.
Padroneggiare questi dettagli permette di sfruttare le strutture dati native di Go in modo consapevole, scrivendo applicazioni più affidabili e mantenibili, dalle API ai processi di elaborazione dei dati.