Stai scrivendo Go da un po'. Usi struct per i dati, slice per le liste, map per i dizionari. Funziona. Ma quando il progetto cresce — gestione dello stato, API condivise, concorrenza — iniziano i dolori: mutazioni inaspettate, interfacce che non incastrano, tipi che scoppiano. Il problema non è Go: è non aver capito il modello di memoria e il contratto di ogni tipo.
Noi di Meteora Web lavoriamo con Go da anni per backend ad alte prestazioni. Abbiamo visto codice dove una slice passata per valore faceva crashare il server, o una map condivisa tra goroutine scatenava data race. Questa guida non è un ripasso da principianti: è un approfondimento operativo su come funzionano davvero struct, interface, slice, map e pointer in Go, con esempi che puoi compilare e testare subito.
Il modello di memoria di Go: valore vs riferimento
Prima di qualsiasi tipo, devi capire una cosa: in Go quasi tutto è passato per valore. Quando assegni una variabile a un’altra o la passi a una funzione, Go copia il valore. Sembra banale, ma è la causa dell’80% degli errori.
Fanno eccezione i tipi con riferimento implicito: slice, map, channel, pointer (ovviamente) e interface. Quando copi una slice o una mappa, copi la struttura dati che punta ai dati, non i dati stessi. Due variabili possono condividere lo stesso array sottostante. Questo è potentissimo, ma anche pericolosissimo se non lo controlli.
Struct: dati aggregati, ma attento alla copia
Una struct è un tipo valore. Se la passi a una funzione, Go ne fa una copia completa — campi, byte, tutto. Per strutture piccole va bene. Per strutture con slice interni o campi grandi, vuoi usare un puntatore.
type Person struct {
Name string
Age int
}
func birthday(p Person) {
p.Age++
}
func main() {
p := Person{Name: "Mario", Age: 30}
birthday(p)
fmt.Println(p.Age) // 30, non 31!
}
Soluzione: passare un puntatore.
func birthday(p *Person) { p.Age++ }Metodi su struct: valore vs puntatore
I metodi in Go possono avere receiver per valore o per puntatore. La scelta cambia tutto: un receiver per valore copia la struct; uno per puntatore no. Regola pratica: se il metodo modifica lo stato, usa pointer receiver. Altrimenti, per strutture piccole (es.
time.Duration) il valore è ok.func (p *Person) HaveBirthday() { p.Age++ } func (p Person) IsAdult() bool { return p.Age >= 18 }Slice: la struttura nascosta
Una
slicenon è un array. È una struttura a tre campi: pointer all'array sottostante, length e capacity. Quando passi una slice a una funzione, passi questa triade (per valore). Ma il puntatore all'array è ancora là: se modifichi gli elementi, modifichi l'array condiviso.Ecco il classico errore:
func modifySlice(s []int) { for i := range s { s[i] *= 2 } s = append(s, 99) // non modifica la slice originale! } func main() { a := []int{1, 2, 3} modifySlice(a) fmt.Println(a) // [2 4 6] — non [2 4 6 99] }Per modificare la slice (aggiungere/rimuovere elementi), devi restituirla o passare un puntatore alla slice.
Slice capacity e append
Quando fai
append, se la capacity è sufficiente, la slice scrive nella stessa area di memoria. Altrimenti ne alloca una nuova (e raddoppia la capacità). Conoscere questo evita allocazioni inutili.s := make([]int, 0, 10) // capacità 10 for i := 0; i < 10; i++ { s = append(s, i) // nessuna allocazione }Map: reference type da trattare con cura
Una
mapin Go è un puntatore a una struttura interna. Passarla per valore equivale a passare un puntatore: se modifichi il contenuto, vedi le modifiche da entrambi i lati. Ma se assegninila una variabile, l’altra non lo sa.func addEntry(m map[string]int, key string, val int) { m[key] = val } func main() { m := map[string]int{"a": 1} addEntry(m, "b", 2) fmt.Println(m) // map[a:1 b:2] }Attenzione: le mappe non sono thread-safe. Se due goroutine scrivono sulla stessa mappa senza sincronizzazione, ottieni fatal error: concurrent map writes. Usa
sync.Mutexosync.Map.Map e puntatori: occhio alla copia
Quando una map ha come valore una struct, se ottieni il valore con
v := m[key], stai copiando la struct. Modificarevnon aggiorna la mappa. Soluzione: usare puntatori come valori, o assegnare di nuovo.type Counter struct{ Value int } func main() { m := map[string]*Counter{} m["page"] = &Counter{} m["page"].Value++ // ok perché è un puntatore }Pointer: il controllo esplicito
I puntatori in Go sono sicuri (nessuna aritmetica come in C), ma vanno usati con consapevolezza. Un puntatore può essere
nil. Dereferenziare un puntatore nil causa panic.Usa i puntatori quando:
- Devi modificare un valore da una funzione (es.
Scan(&x)) - La struttura dei dati è grande e vuoi evitare copie
- Devi rappresentare l’assenza di un valore (es.
var p *int = nil)
Non usare puntatori per valori piccoli e immutabili (int, bool). In Go, i puntatori possono creare complessità inutili e aumentare la pressione sul garbage collector.
Interface: il contratto implicito
Un’interface in Go è un tipo composto da due puntatori: il tipo dinamico e il valore dinamico. Quando assegni un valore a un’interfaccia, Go memorizza il tipo e una copia del valore (o un puntatore, se il valore è un puntatore).
Le interfacce sono soddisfatte implicitamente: se un tipo implementa i metodi dichiarati, automaticamente implementa l’interfaccia. Questo è flessibile, ma può portare a errori sottili.
Il nil non è sempre nil
Una variabile di tipo interfaccia può essere nil solo se sia il tipo che il valore sono nil. Se assegni un puntatore nil a un’interfaccia, l’interfaccia non è nil — ha un tipo (*Person) e un valore nil. Questo rompe i controlli.
var p *Person = nil
var i interface{} = p
fmt.Println(i == nil) // false!
Morale: non controllare err != nil se err può essere un’interfaccia che avvolge un valore nil. Meglio restituire nil esplicito per l’interfaccia.
Interface vuote e type assertion
L’interfaccia vuota interface{} accetta qualsiasi tipo. Non usarla come tipizzazione dinamica selvaggia — Go non è JavaScript. Per lavorare con dati eterogenei, usa any (alias di interface{} in Go 1.18+) e type switch.
func detect(v any) {
switch val := v.(type) {
case int:
fmt.Println("int:", val)
case string:
fmt.Println("string:", val)
default:
fmt.Println("unknown")
}
}
Quando usare puntatori e quando valori nei tipi composti
Non esiste una regola unica. Noi seguiamo queste linee guida:
- Struct che rappresentano entità con identità (es. utente, ordine): usa puntatori o sempre puntatori (es.
*User). - Struct piccole e immutabili (es. coordinate, colore): passa per valore.
- Slice di struct: se devi modificare gli elementi in-place, usa
[]*T. Se no,[]Tè più cache-friendly. - Mappe di struct: quasi sempre
map[K]*Vper evitare copie. - Interfacce: evita di memorizzare puntatori a interfacce. Le interfacce sono già puntatori al valore reale.
Cosa fare adesso
- Rivedi il codice Go che hai scritto — cerca passaggi di slice e mappe a funzioni, e chiediti: “cosa succede se modifico qui?”.
- Testa la differenza tra value e pointer receiver su una struct di medie dimensioni (es. 20 campi). Confronta le allocazioni con
go test -benchmem. - Applica le type assertion con cautela: mai fare
v.(T)senza il secondo valore booleano, o usare unswitch. - Struttura le interfacce per comportamento, non per dati. Un’interfaccia con un solo metodo (
Reader,Writer) è oro. - Metti in mutex le mappe condivise tra goroutine. O meglio, usa
sync.Mapper casi specifici (letture intensive, scritture rare).
Noi di Meteora Web abbiamo visto codebase Go diventare un incubo quando questi meccanismi vengono ignorati. Ma con la consapevolezza giusta, diventano strumenti potentissimi. Buon codice.
Sponsored Protocol