Funzioni anonime
Un letterale di funzione è una funzione senza nome. È un'espressione, quindi può stare ovunque possa stare un valore: una variabile, un argomento, un valore di ritorno, un campo di una struct.
Go non ha una forma abbreviata con freccia o lambda. La forma completa func(params) results { body } è l'unico modo per scriverne una.
Catturare variabili
Un letterale di funzione può usare le variabili della funzione che lo racchiude. Non le copia: la closure e il codice circostante condividono una sola variabile.
La variabile catturata vive finché una closure fa ancora riferimento a essa, anche dopo che la funzione che l'ha dichiarata è ritornata. Il compilatore la sposta sull'heap quando serve; non devi mai gestirlo tu.
Generatori e contatori
Restituire una closure da una funzione dà a ogni funzione restituita un proprio stato privato.
Ogni chiamata a counter() crea un nuovo n, quindi c1 e c2 contano in modo indipendente. Niente all'esterno può leggere o azzerare n, il che rende questa una piccola forma di incapsulamento.
Closure nei cicli (Go 1.22 e successivi)
Prima di Go 1.22, un ciclo for aveva una sola variabile di ciclo per l'intero ciclo. Le closure create al suo interno catturavano tutte quell'unica variabile e ne vedevano il valore finale. Era il bug più comune con le closure in Go.
Da Go 1.22 ogni iterazione riceve la propria copia, quindi il codice qui sotto stampa 0 1 2:
Con Go 1.21 e precedenti, lo stesso ciclo scritto come for i := 0; i < 3; i++ (anche il range su un intero è una novità della 1.22) stampava 3 3 3. La nuova regola si applica per modulo, in base alla riga go di go.mod, quindi un vecchio modulo mantiene il comportamento precedente finché la sua riga go non viene portata a 1.22 o oltre. Nel codice più vecchio vedrai ancora i := i o v := v dentro i cicli: era la correzione manuale e ora è superflua.
La regola per iterazione copre solo le variabili dichiarate dall'istruzione for stessa. Una variabile dichiarata prima del ciclo e aggiornata nel corpo resta un'unica variabile condivisa:
var last string
for _, s := range items {
last = s
handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}
Closure e goroutine
Anche una goroutine avviata con un letterale di funzione è una closure. Valgono le stesse regole di condivisione, più la solita regola della concorrenza: se più goroutine scrivono una variabile catturata, ti serve un mutex o un channel.
Questo stampa sempre 5050. Ogni goroutine legge il proprio i (semantica dei cicli di Go 1.22) e scrive il total condiviso sotto un lock. Togli il mutex e il risultato diventa imprevedibile; go run -race lo segnala come data race. Consulta goroutine per WaitGroup e il race detector.
Dove compaiono le closure nel codice reale
- Ordinamento e ricerca:
slices.SortFunc,slices.IndexFunc,sort.Sliceaccettano tutte un letterale di funzione. - Pulizia differita:
defer func() { ... }()esegue un blocco all'uscita dalla funzione e può leggere le variabili della funzione. Consulta defer. - Middleware HTTP: una funzione che riceve un handler e restituisce un nuovo letterale
http.HandlerFuncche lo avvolge. - Configurazione: le opzioni funzionali (
func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }) sono closure sull'argomento.
Un esempio di middleware che cattura sia un parametro sia un contatore:
Errori comuni
- Aspettarsi un'istantanea. Una closure legge il valore attuale della variabile quando viene eseguita, non il valore che aveva quando la closure è stata creata. Passa il valore come parametro se ti serve un'istantanea:
go func(v int) { ... }(x). - Letterali ricorsivi. Un letterale di funzione non può riferirsi a sé stesso tramite la variabile a cui viene assegnato nello stesso
:=. Dichiara prima la variabile:var walk func(n int); walk = func(n int) { ... walk(n-1) }. - Scritture non sincronizzate dalle goroutine. Catturare non significa sincronizzare. Proteggi le scritture condivise.
Domande frequenti
Cos'è una funzione anonima in Go?
Un letterale di funzione senza nome: func(x int) int { return x * 2 }. Puoi assegnarlo a una variabile, passarlo come argomento, restituirlo o chiamarlo subito aggiungendo () dopo la parentesi graffa di chiusura. Go non ha una sintassi lambda separata; il letterale di funzione è la lambda.
Cos'è una closure in Go?
Un letterale di funzione che fa riferimento a variabili dichiarate fuori da esso. La funzione mantiene in vita quelle variabili e le condivide con lo scope che la racchiude, quindi le modifiche fatte dentro la closure sono visibili fuori e viceversa. Le variabili vengono catturate per riferimento, non copiate.
Le closure di Go catturano le variabili per valore o per riferimento?
Per riferimento. La closure e il codice circostante usano la stessa variabile. Se ti serve un'istantanea, copia il valore in una nuova variabile prima di creare la closure, oppure passalo come argomento.
Il bug della variabile di ciclo nelle closure è risolto in Go?
Sì, da Go 1.22. Ogni iterazione di un ciclo for ora dichiara una nuova variabile di ciclo, quindi closure e goroutine create nel ciclo vedono il valore di quella iterazione. Vale per i moduli il cui go.mod indica go 1.22 o successivo. Il codice più vecchio contiene spesso i := i dentro il ciclo come soluzione alternativa; non fa danni ma non serve più.