Go - from Google


Puntatori     

Ecco ad un argomento per alcuni versi un po' spinoso, soprattutto per chi non hanno questa chiamiamola "feature". I puntatori sono famigliari a chi bazzica le rive di linguaggi come C e C++ mentre risultano un po' più alieni ad esempio per chi adopera C#, Java, Python o altri linguaggi che sacrificando l'efficienza in favore della semplicità cercano di eitare che il programmatore si imbatta in questi utili ma delicati operatori. D'altronde, come accennato, se si vogliono performance all'altezza e un controllo superiore sulla memoria i puntatori sono una via quasi obbligata e comunque Go smussa gli spigoli che si incontrano nel loro uso allontanando il programmatore dalla loro "aritmetica", quindi, ad esempio, quelle operazioni tipiche del C che permettono di "spostarsi" partendo da una locazione indicata da un puntatore tramite operazioni sul puntatore stesso non sono permesse. Sicuramente insomma non siamo nella situazione di "pericolo costante" in cui si ritrova chi programma in C anche se niente deve essere sottovalutato, come sempre, anche quando si lavora con Go. Il discorso non è banale in realtà, lo incontreremo spesso. In questo paragrafo diamo le nozioni di base che saranno utili per la migliore comprensione in altri ambiti. Come intuitivo i puntatori sono particolarmente utili con i tipi valore; per quanto riguarda i reference types, come slice, funzioni, channels ecc,  anche ad essi ci si può riferire tramite puntatori; in realtà solo con slices questo può risultare davvero utile.

Per iniziare, vediamo un concetto semplice, presentando il primo operatore, & chiamato address operator (non sto a tradurlo... tra l'altro, giusto per cultura, & è overloaded dal momento che può essere usato su operatori binari per realizzari un bitwise and) Esso, posto prima di una variabile, ci restituisce l'indirizzo di memoria, nel suo formato esadecimale, occupato dalla variabile stessa. D'altronde un puntatore, per definizione, è proprio una variabile che contiene l'indirizzo di memoria di un'altra variabile. Le dimensioni di un puntatore non dipendono dalla variabile che è puntata bensì dall'indirizzamento del sistema operativo sottostante, quindi abbiamo 4 bytes in un ambiente 32 bits e 8 in un ambiente a 64. L'esempio che segue è semplicissimo:

  Esempio 12.1
1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
r := 0
fmt.Println(&r)
}

Il risultato varia da compuer a computer e anche in esecuzioni successive, di solito. Sulla mia macchina ecco l'output di alcune esecuzioni in sequenza (pointer01 è il nome del mio programma di test)

C:\Go\bin>pointer01
0x12060038

C:\Go\bin>pointer01
0x12160038

C:\Go\bin>pointer01
0x12330038

C:\Go\bin>pointer01
0x12280038

C:\Go\bin>pointer01
0x121d0038


da cui si deduce che l'allocazione in memoria della variabile r può essere o meno diversa e normalmente lo è. Il risultato come detto è espresso in formato esadecimale. Il risultato può essere memorizzato in una variabile di tipo puntatore:

var p *int

il cui formato generico è, evidentemente:
*tipo
e che contiene un indirizzo di memoria.
Da notare, facendo un po' di best practice, che è buona cosa lasciare uno spazio tra il nome della variabile e l'asterisco in quanto diversamente o si avrebbe un errore o si potrebbe , in qualche caso, indurre in errore chi leggesse il codice che potrebbe vederci una moltiplicazione. Anche * è  oveloaded visto che lo usiamo anche per le moltiplicazioni. Comunque, in base a quanto detto finora, possiamo scrivere istruzioni come le seguenti:

x := 5
p *int
p = &x


Quindi:

  Esempio 12.2
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
r := 0
fmt.Println(&r)
var p *int
p = &r
fmt.Println(p)
}

La variabile puntatore contiene, è bene ribadirlo, l'indirizzo di memoria in cui si trova il valore.
Il seguente programma allarga un po' il campo d'uso:

  Esempio 12.3
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
r := 88
fmt.Println(&r)
var p *int
p = &r
fmt.Println(p)
fmt.Println(*(&p))
fmt.Println(*p)
}

con il seguente output:

0x12410038
0x12410038
0x12410038
88


I primi due risultati sono ovvi e derivano dalle righe 7 e 10. Alla riga 11 e alla 12 abbiamo la cosiddetta dereferenziazione che ci restituisce il valore a cui il puntatore sta puntando inteso anche, vedasi l'esempio riga 11, come indirizzo di memoria a cui sta puntando. La riga 12 recupera il valore puntato, similmente direi. L'ultima istruzione ci permette anche di capire come cambiare il valore di una variabile tramite puntatore; banalmente:

*p = 100

cambia il valore di r. Si possono fare altre operazioni, ad esempio incremento, decremento o quello che volete, l'importante è ricordare che il tipo di dato a cui il cui il puntatore punta supporti l'operazione che volete fare. Tuttavia non si può usare il valore nil: dereferenziare a questo valore speciale causa un crash del vostro programma. Ovvero non è possibile scrivere una inizializzazione di questo tipo:

var p *int = nil
 
Come abbiamo già visto nel capitolo dedicato alle funzioni, tramite i puntatori è possibile modificare i parametri passati ad una funzione. Lo ripetiamo con un semplice esempio:

  esempio 12.4a
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func f (x int) {
x = 10
}

func main() {
x := 11
f(x)
fmt.Println(x)
}
  Esempio 12.4b
1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func f (x *int) {
*x = 10
}

func main() {
x := 11
f(&x)
fmt.Println(x)
}

Il codice dell'esempio 12.4a dà come risultato 11 in quanto il valore x all'interno del main non viene toccato. Eseguendo invece il 12.4 ricaviamo come risultato 10. In questo caso infatti passiamo non un valore, che viene copiato e quindi lascia inalterato l'elemento originale, ma un puntatore, ovvero una locazione di memoria che viene poi ripresa alla riga 6 per attribuire il nuovo valore.

Ovviamente un puntatore può puntare qualsiasi elemento, è sufficiente conoscere la locazione di memoria dell'elemento; quindi si possono anche avere puntatori di puntatori (e così via definendo vari livelli di indirettezza, come segnalato anche nella documentazione ufficiale). E' possibile scrivere pertanto istruzioni come le seguenti:

x := 1
p1 := &x
p2 := &p1
fmt.Println(x)
fmt.Println(p1)
fmt.Println(p2)
fmt.Println(x, *p1, **p2)


l'ultima istruzione darà come risultato

1 1 1

Un modo alternativo per creare un puntatore è utilizzando la keyword new in congiunzione con un tipo. Tramite new non facciamo altro che allocare spazio per accogliere un elemento del tipo indicato e creare un puntatore alla zona di memoria dedicata. L'esempio che segue è in pratica il 12.4b modificato tramite l'uso di new.

  Esempio 12.5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func f (x *int) {
*x = 10
}

func main() {
p1 := new(int)
fmt.Println(*p1)
f(p1)
fmt.Println(*p1)
}

Come si vede il funzionamento è identico all'esempio precedente, a parte l'attribuzione di un valore iniziale che si sarebbe comunque potuto banalmente fare tramite ad esempio dereferenziazione.
Puntatori e strutture sono ottimi compagni di viaggio. Le strutture infatti possono essere onerose per essere passata ad esempio come parametri ed ecco che l'uso dei puntatori per la loro gestione dimostra tutta la sua grande potenza ed effcienza. Le strutture possono essere trattate infatti direttamente, ovvero istanziate, oppure ci si può rivolgere ad esse tramite puntatori. L'esempio seguente mostra la differenza:

  Esempio 12.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

type builder struct {
x1 int
x2 int
}


func main() {
p1 := builder{1,2}
p2 := new(builder)
p2.x1 = 4
p2.x2 = 5
fmt.Println(p1)
fmt.Println(p2)
p3 := &builder{}
p3.x1 = 6
p3.x2 = 7
fmt.Println(p3)
}

La riga 12 costruisce direttamente una istanza della struct builder e la riga 16 ne stampa i valori. La riga 13 invece definisce un puntatore. L'output pertanto è il seguente:

{1 2}
&{4 5}
&{6 7}


Come si vede Go ci indica chiaramente che, nel secondo caso, riferito a p2 sta lavorando con puntatori non con valori. La riga 18 ci indica un modo alternativo per ripetere quanto scritto alla riga 13. Il riusltato è assolutamente identico in termini pratici. La sintassi presentata alla riga 18 tuttavia è in generale preferibile in quanto più elastica e ci permetterebbe di inizializzare i valori di p3 in modo del tutto identico a qaunto fatto alla riga 12, ovvero il modo migliore per scrivere il codice dalla riga 18 alla 20 sarebbe:

p3 := &builder{6,7}

Come detto l'argomento è tutt'altro che completamente sviscerato, lo abbiamo già incontrato e lo incontreremo ancora. Tuttavia le basi sono in questo paragrafo. L'invito è quello di fare qualche esprimento ulteriore usando magari più livelli di indirezione per comprendere i meccanismi.