& e *
Un puntatore contiene l'indirizzo di memoria di un valore. &x ti dà un puntatore a x. *p segue il puntatore fino al valore.
L'asterisco ha due ruoli. In un tipo (*int) significa "puntatore a int". In un'espressione (*p) significa "il valore a cui punta p". Stampare p da solo mostra un indirizzo come 0xc000012345, che cambia da un'esecuzione all'altra.
Perché esistono i puntatori: condividere invece di copiare
Go passa tutto per valore. Una funzione riceve una copia di ogni argomento, quindi non può cambiare la variabile di chi la chiama. Passare un puntatore copia invece l'indirizzo, ed entrambe le parti raggiungono lo stesso valore.
Go non ha riferimenti nel senso del C++. In Go "passare per riferimento" significa passare un puntatore per valore.
Puntatori a struct
I puntatori a struct sono i puntatori più comuni nel codice Go. L'accesso ai campi dereferenzia in automatico: p.Name significa (*p).Name.
I metodi seguono la stessa logica: un receiver puntatore permette al metodo di cambiare la struct. La pagina sui metodi tratta la scelta tra receiver valore e receiver puntatore.
new
new(T) alloca un T azzerato e restituisce il suo indirizzo. È come dichiarare una variabile e prenderne l'indirizzo.
p := new(int) // *int pointing at 0
var x int
q := &x // same thing, two lines
a := new(Account) // same as &Account{}
In pratica per le struct è più comune &T{...}, perché permette di impostare i campi nella stessa espressione. new è comodo per i puntatori a tipi di base. Non confonderlo con make, che costruisce slice, map e channel e li restituisce pronti all'uso, non come puntatori.
Restituire un puntatore a una variabile locale è sicuro
In C, restituire l'indirizzo di una variabile locale è un bug. In Go è codice normale.
L'escape analysis del compilatore decide se un valore vive sullo stack o sull'heap. Non scegli mai tu, e non liberi mai la memoria: il garbage collector la recupera quando niente vi punta più. go build -gcflags=-m stampa queste decisioni, se sei curioso.
Puntatori nil
Lo zero value di qualsiasi tipo puntatore è nil. Dereferenziare nil va in panic.
Output:
1
2
true
recovered: runtime error: invalid memory address or nil pointer dereference
Senza il recover, il programma crasherebbe con quel messaggio e uno stack trace che inizia con panic: runtime error: invalid memory address or nil pointer dereference e [signal SIGSEGV: segmentation violation ...]. Le fonti abituali:
- una variabile puntatore o un campo di una struct mai impostati
m[key]su unamap[string]*Tper una chiave mancante, che restituiscenil- ignorare un errore:
f, _ := os.Open(path)lasciafa nil in caso di fallimento - un metodo con receiver puntatore chiamato su un puntatore nil, che poi legge un campo
Chiamare un metodo su un puntatore nil è di per sé permesso. Il panic avviene solo quando il metodo tocca i campi del receiver.
Quando usare un puntatore
Usa un puntatore quando:
- una funzione o un metodo deve cambiare il valore di chi chiama
- il valore è una struct grande e viene passato in giro spesso
- il tipo non deve essere copiato (contiene un
sync.Mutex, o rappresenta una risorsa unica come una connessione) - ti serve esprimere "nessun valore" con
nil, per esempio un campo facoltativo in una struct JSON (*intdistingue "assente" da0)
Non usare un puntatore quando:
- il valore è piccolo e di sola lettura (
time.Time, unPoint) - il tipo si comporta già come un riferimento: slice, map, channel, funzioni e interface raramente hanno bisogno di un
*davanti. Un*[]into una*map[string]intsono quasi sempre un errore
I puntatori non sono automaticamente più veloci. Copiare una struct piccola costa poco, e un puntatore può forzare un'allocazione sull'heap e aggiungere lavoro al garbage collector.
Puntatori e variabili del ciclo
Prendere l'indirizzo di una variabile del ciclo è sicuro da Go 1.22, perché ogni iterazione ha la sua variabile:
Stampa 0 1 2. Prima di Go 1.22 (che ha introdotto anche range 3), lo stesso ciclo scritto come for i := 0; i < 3; i++ stampava 3 3 3, perché tutti e tre i puntatori condividevano una sola variabile. Nota che &v in for _, v := range items punta a una copia dell'elemento, non all'elemento nella slice. Per avere un puntatore all'elemento stesso, usa &items[i].
Errori comuni
- Dereferenziare senza controllare. Controlla
if p == nilovunque nil sia possibile. - Puntatore al valore di un range.
&vpunta a una copia; usa&s[i]. - Puntatori a slice e map. Servono raramente. Restituisci invece la nuova slice.
- Tenere
&s[i]attraverso unappend. Seappendrialloca, il puntatore fa ancora riferimento al vecchio array e le scritture successive finiscono in un posto che nessuno vede.
Domande frequenti
Cosa significano & e * in Go?
&x prende l'indirizzo di x e dà un puntatore di tipo *T. *p dereferenzia il puntatore: legge o scrive il valore a cui punta. In un tipo, *T significa "puntatore a T".
Qual è la differenza tra new e make in Go?
new(T) alloca un T azzerato e restituisce un *T; funziona con qualsiasi tipo. make funziona solo con slice, map e channel, e restituisce un valore inizializzato (non azzerato, non un puntatore) di quel tipo: make(map[string]int) è pronto all'uso, mentre new(map[string]int) è un puntatore a una map nil.
Posso restituire un puntatore a una variabile locale in Go?
Sì. A differenza del C, è sicuro. L'escape analysis del compilatore vede che la variabile sopravvive alla funzione e la alloca sull'heap. func newInt() *int { x := 5; return &x } è Go corretto.
Cosa causa "invalid memory address or nil pointer dereference" in Go?
Leggere o scrivere attraverso un puntatore nil: una variabile *T mai assegnata, la ricerca di una chiave mancante in una map[K]*V, o una funzione che ha restituito nil insieme a un errore che non hai controllato. Controlla se è nil, oppure inizializza il puntatore prima di usarlo.
Go ha l'aritmetica dei puntatori?
No. Non puoi sommare numeri a un puntatore né indicizzare la memoria attraverso di esso. Il package unsafe lo permette per il codice di basso livello, senza garanzie di sicurezza. Il codice Go normale usa le slice per la memoria contigua.