ProgrammazioneSenior Go Developer

Racconta come Go implementa le chiusure (func literals/closures) e quali sono i limiti e le caratteristiche dell'uso delle chiusure: dove vengono memorizzate, come vengono catturate le variabili, come si differenzia il comportamento delle variabili catturate in scenari diversi?

Supera i colloqui con l'assistente IA Hintsage

Risposta

In Go, le funzioni anonime (func literals) sono in grado di creare chiusure, ovvero accedere alle variabili dall'ambiente circostante anche dopo la sua conclusione. Tali chiusure allocano memoria nel heap, se necessario per il corretto funzionamento (rilevato tramite l'analisi di escape).

Esempio:

func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } a := adder() printf("%d\n", a(5)) // 5 printf("%d\n", a(10)) // 15

Caratteristiche:

  • La chiusura cattura le variabili esterne per riferimento (non i loro valori al momento della creazione).
  • Se una variabile viene modificata al di fuori della chiusura, la chiusura vedrà il nuovo valore.
  • Se la chiusura viene restituita da una funzione, le variabili catturate vivranno fino alla fine della vita della chiusura.
  • Se la chiusura non viene utilizzata, l'analisi di escape potrebbe consentire alle variabili di non andare nell'heap.

Domanda trabocchetto

Cosa stamperà questo codice?

func main() { fs := []func(){} for i := 0; i < 3; i++ { fs = append(fs, func() { fmt.Println(i) }) } for _, f := range fs { f() } }

Molti risponderanno che stamperà 0, 1, 2, tuttavia il risultato sarà:

3
3
3

Tutte le chiusure fanno riferimento alla stessa variabile i; dopo la conclusione del ciclo, il suo valore è 3.

Corretto: catturare una copia della variabile nel corpo del ciclo:

for i := 0; i < 3; i++ { v := i // nuova variabile fs = append(fs, func() { fmt.Println(v) }) }

Esempi di errori reali causati dall'ignorare le sottigliezze dell'argomento


Storia

Nel progetto di routing dinamico, è stato usato un ciclo per creare molti handler tramite closure, ognuno doveva catturare il proprio percorso. Di conseguenza, tutti gli handler stampavano l'ultimo percorso — non è stata creata una variabile separata in ogni chiusura. L'errore è stato scoperto solo durante l'integrazione con l'API HTTP.


Storia

Testando l'accesso concorrente tramite goroutine all'interno di un ciclo, la closure catturava il riferimento all'indice, non una copia. Questo creava effetti "casuali": i dati venivano scritti non nel proprio slot dell'array, ma nell'ultimo.


Storia

Nella funzione di raccolta statistiche, la closure modificava una variabile globale dall'ambiente esterno, anche se l'autore si aspettava un contatore indipendente per ciascun task. Il problema è stato notato a causa di una somma ricostruita in modo inadeguato, che era sempre globale, non privata, nonostante la logica locale.