f in x
Tipi in Go: struct, interface, slice, map e pointer — Guida avanzata per un codice robusto e performante
> cd .. / HUB_EDITORIALE > Visualizza in Inglese
Analisi dei dati e metriche

Tipi in Go: struct, interface, slice, map e pointer — Guida avanzata per un codice robusto e performante

[2026-06-04] Author: Ing. Calogero Bono

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 slice non è 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 map in 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 assegni nil a 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.Mutex o sync.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. Modificare v non 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]*V per evitare copie.
  • Interfacce: evita di memorizzare puntatori a interfacce. Le interfacce sono già puntatori al valore reale.

Cosa fare adesso

  1. Rivedi il codice Go che hai scritto — cerca passaggi di slice e mappe a funzioni, e chiediti: “cosa succede se modifico qui?”.
  2. Testa la differenza tra value e pointer receiver su una struct di medie dimensioni (es. 20 campi). Confronta le allocazioni con go test -benchmem.
  3. Applica le type assertion con cautela: mai fare v.(T) senza il secondo valore booleano, o usare un switch.
  4. Struttura le interfacce per comportamento, non per dati. Un’interfaccia con un solo metodo (Reader, Writer) è oro.
  5. Metti in mutex le mappe condivise tra goroutine. O meglio, usa sync.Map per 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

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Co-founder di Meteora Web. Ingegnere informatico, sviluppo ecosistemi digitali ad alte prestazioni. AI, automazione, SEO tecnica e infrastrutture web. Scrivo di tecnologia per rendere complesso… semplice.

[ Read Full Dossier ]

Hai bisogno di applicare questa strategia?

Esegui il protocollo di contatto per iniziare un progetto con noi.

> INIZIA_PROGETTO

Sponsored

> MW_JOURNAL

> READ_ALL()