Come molti altri linguaggi, Go ha i puntatori. Se è la prima volta che hai a che fare con i puntatori, possono sembrare un po' misteriosi, ma una volta che ci prendi l'abitudine diventano piuttosto semplici. Sono una parte fondamentale di Go, quindi prenditi un po' di tempo per capirli davvero.
Prima di addentrarci nei dettagli, vale la pena capire a cosa servono i puntatori. I puntatori sono un modo per condividere la memoria con altre parti del nostro programma, e questo è utile per due motivi principali:
Diciamo che abbiamo una normale variabile di tipo intero a:
var a int
Quando dichiariamo una variabile, Go deve trovare un posto in memoria dove memorizzarne il valore. Questo aspetto è in gran parte astratto per noi: quando dobbiamo recuperare il valore memorizzato in quella zona di memoria, possiamo semplicemente riferirci a essa usando il nome della variabile.
Per esempio, quando scriviamo a + 2, in pratica recuperiamo il valore memorizzato nella memoria associata alla variabile a e vi aggiungiamo 2.
Allo stesso modo, quando dobbiamo cambiare il valore nella zona di memoria di a, possiamo usare il nome della variabile per fare un'assegnazione:
a = 3
La zona di memoria associata ad a ora memorizzerà il valore 3.
Mentre le variabili ci permettono di riferirci ai valori in memoria, a volte è utile conoscere l'indirizzo di memoria verso cui punta la variabile. I puntatori contengono gli indirizzi di memoria di quei valori. Dichiari una variabile di tipo puntatore anteponendo un asterisco al tipo sottostante:
var p *int // 'p' contains the memory address of an integer
Qui dichiariamo una variabile p di tipo «puntatore a int» (*int).
Questo significa che p conterrà l'indirizzo di memoria di un intero.
Il valore zero dei puntatori è nil, perché un puntatore nil non contiene alcun indirizzo di memoria.
Per trovare l'indirizzo di memoria del valore di una variabile, possiamo usare l'operatore &.
Per esempio, se vogliamo trovare e memorizzare l'indirizzo di memoria della variabile a nel puntatore p, possiamo fare così:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
Quando abbiamo un puntatore, potremmo voler conoscere il valore memorizzato nell'indirizzo di memoria che il puntatore rappresenta.
Possiamo farlo usando l'operatore *:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
var b int
b = *p // b == 2
L'operazione *p recupera il valore memorizzato all'indirizzo di memoria contenuto in p.
Questa operazione viene spesso chiamata «dereferenziazione».
Possiamo anche usare l'operatore di dereferenziazione per assegnare un nuovo valore all'indirizzo di memoria a cui il puntatore fa riferimento:
var a int // declare int variable 'a'
a = 2 // assign 'a' the value of 2
var pa *int
pa = &a // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2 // increment by 2 the value at memory address 'pa'
fmt.Println(a) // Output: 4
// 'a' will have the new value that was changed via the pointer!
Assegnare a *pa cambierà il valore memorizzato all'indirizzo di memoria contenuto in pa.
Dato che pa contiene l'indirizzo di memoria di a, assegnando a *pa stiamo di fatto cambiando il valore di a!
Un avvertimento, però: controlla sempre che un puntatore non sia nil prima di dereferenziarlo.
Dereferenziare un puntatore nil farà andare in crash il programma a runtime!
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
Finora abbiamo visto solo puntatori a valori primitivi. Possiamo anche creare puntatori a struct:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
Avremmo anche potuto creare una nuova Person e memorizzare subito un puntatore a essa:
var p *Person
p = &Person{Name: "Peter", Age: 22}
Quando abbiamo un puntatore a una struct, non serve dereferenziare il puntatore prima di accedere a uno dei campi:
var p *Person
p = &Person{Name: "Peter", Age: 22}
fmt.Println(p.Name) // Output: "Peter"
// Go automatically dereferences 'p' to allow
// access to the 'Name' field
Le slice e le map sono tipi speciali, perché nella loro implementazione hanno già dei puntatori. Questo significa che, il più delle volte, non abbiamo bisogno di creare puntatori per questi tipi per condividere l'indirizzo di memoria dei loro valori. Immaginiamo di avere una funzione che incrementa il valore di una chiave in una map:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
Se creiamo una map e chiamiamo questa funzione, le modifiche che la funzione ha fatto alla map rimangono dopo che la funzione è terminata. È un comportamento simile a quello che otterremmo usando un puntatore, ma nota come in questo esempio non stiamo usando alcun riferimento, alcuna dereferenziazione o altra parte della sintassi dei puntatori:
ages := map[string]int{
"Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!
Lo stesso vale quando modifichiamo un elemento esistente in una slice.
Tuttavia, le azioni che restituiscono una nuova slice, come append, sono un caso a parte e potrebbero non modificare la slice al di fuori della funzione.
Questo dipende da come funzionano le slice internamente, ma non lo tratteremo in dettaglio in questo esercizio, perché è un argomento più avanzato.
Se sei davvero curioso, puoi leggere di più al riguardo in Go Blog: Mechanics of 'append'
Nella tua zona c'è una scuola con un'associazione studentesca molto attiva. L'associazione studentesca è gestita da un presidente e ogni 2 anni si tengono le elezioni per eleggere un nuovo presidente.
Quest'anno si è deciso che serviva un nuovo sistema digitale per contare i voti. La scuola ha bisogno del tuo aiuto per costruire questo nuovo sistema.
Una delle prime cose di cui il nuovo sistema di voto ha bisogno è un contatore di voti. Questo contatore serve a tenere traccia dei voti che ha un particolare candidato.
Crea una funzione NewVoteCounter che accetta il numero di voti iniziali di un candidato e restituisce un puntatore a un int, inizializzato con il numero di voti iniziali dato.
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
Ora hai un modo per creare nuovi contatori! Ma ti accorgi che il nuovo sistema avrà bisogno anche di un modo per ottenere il numero di voti da un contatore.
Crea una funzione VoteCount che prende un contatore (*int) come argomento e restituisce il numero di voti nel contatore.
Se il contatore è nil, devi assumere che non abbia voti:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
È finalmente il momento di contare i voti! Ora ti serve un modo per incrementare i voti in un contatore.
Crea una funzione IncrementVoteCount che prende un contatore (*int) come argomento e un numero di voti, e incrementa il contatore di quel numero di voti.
Puoi assumere che il puntatore passato non sarà mai nil.
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
Con tutti i voti ormai contati, è il momento di preparare l'annuncio dei risultati per tutta la scuola. Per farlo, ti accorgi che avere solo i contatori dei voti non basta. Serve un modo per associare il numero di voti a un particolare candidato.
Crea una funzione NewElectionResult che riceve il nome di un candidato e il suo numero di voti e
restituisce un nuovo risultato elettorale.
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
La struct del risultato elettorale è già stata creata per te ed è definita come:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
È il momento di annunciare il nuovo presidente alla scuola!
Il presidente sarà annunciato nei piccoli tabelloni digitali che la scuola ha a disposizione.
Il messaggio dovrebbe mostrare il nome del nuovo presidente e i voti che ha ottenuto, nel seguente formato: <candidate_name> (<votes>).
Questo è un esempio di tale messaggio: "Peter (51)".
Crea una funzione DisplayResult che riceve un *ElectionResult come argomento e restituisce una stringa con il messaggio da mostrare.
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
Per assicurarsi che i risultati finali fossero accurati, i voti sono stati ricontati. Nel riconteggio è emerso che il numero di voti di alcuni candidati era sbagliato di uno.
Crea una funzione DecrementVotesOfCandidate che riceve i risultati finali e il nome di un candidato per il quale devi decrementare il conteggio dei voti.
I risultati finali sono dati sotto forma di una map[string]int, dove le chiavi sono i nomi dei candidati e i valori sono il totale dei loro voti.
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9
Iscriviti a Exercism per imparare e padroneggiare Go con 34 concetti165 esercizi e il mentoring di persone reali, tutto gratis.