Controllare con le funzioni defer, panic e recover

Completato

Esaminare ora alcuni flussi di controllo univoci per Go: defer, panic e recover. Ognuna di queste funzioni presenta diversi casi d'uso. In questo documento verranno esaminati quelli più importanti.

Funzione defer

In Go, un'istruzione defer posticipa l'esecuzione di una funzione (inclusi eventuali parametri) fino al termine della funzione che contiene l'istruzione defer. In genere, occorre posticipare una funzione quando si vuole evitare di dimenticare attività come la chiusura di un file o l'esecuzione di un processo di pulizia.

È possibile posticipare tutte le funzioni desiderate. Le istruzioni defer vengono eseguite in ordine inverso, dall'ultima alla prima.

È possibile verificare il funzionamento di questo modello eseguendo il codice di esempio seguente:

package main

import "fmt"

func main() {
    for i := 1; i <= 4; i++ {
        defer fmt.Println("deferred", -i)
        fmt.Println("regular", i)
    }
}

Ecco l'output del codice:

regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1

In questo esempio si noti che per ogni posticipo di fmt.Println("deferred", -i) viene archiviato il valore i e la relativa chiamata di funzione viene aggiunta a una coda. Al termine della stampa dei valori regular da parte della funzione main() vengono eseguite tutte le chiamate posticipate. Si noti che l'output delle chiamate posticipate è in ordine inverso (ultima ad arrivare, prima a uscire), man mano che vengono rimosse dalla coda.

Un caso d'uso tipico della funzione defer è la chiusura di un file quando non serve più. Ecco un esempio:

package main

import (
    "io"
    "os"
    "fmt"
)

func main() {
    newfile, error := os.Create("learnGo.txt")
    if error != nil {
        fmt.Println("Error: Could not create file.")
        return
    }
    defer newfile.Close()

    if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {
	    fmt.Println("Error: Could not write to file.")
        return
    }

    newfile.Sync()
}

Dopo aver creato o aperto un file, si posticipa la funzione .Close() per evitare di dimenticare di chiudere il file al termine della modifica.

Funzione panic

Gli errori di runtime attivano la funzione panic per un programma Go, ad esempio quando si prova ad accedere a una matrice usando un indice non compreso nei limiti o si esegue la dereferenziazione di un puntatore nil. È anche possibile forzare l'attivazione della funzione panic per un programma.

La funzione panic() predefinita arresta il normale flusso di controllo in un programma Go. Quando si usa una chiamata a panic, tutte le chiamate di funzione posticipate vengono eseguite normalmente. Il processo continua a risalire lo stack finché tutte le funzioni non vengono completate. Il programma si arresta quindi in modo anomalo con un messaggio di log. Il messaggio include eventuali informazioni sull'errore e un'analisi dello stack che consentono di diagnosticare la causa radice del problema.

Quando si chiama la funzione panic(), è possibile aggiungere qualsiasi valore come argomento. In genere, viene inviato un messaggio di errore relativo al motivo del panico.

Ad esempio, il codice seguente combina le funzioni panic e defer. Provare a eseguire questo codice per osservare l'arresto del flusso di controllo. Si noti che i processi di pulizia sono ancora in esecuzione.

package main

import "fmt"

func highlow(high int, low int) {
    if high < low {
        fmt.Println("Panic!")
        panic("highlow() low greater than high")
    }
    defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
    fmt.Println("Call: highlow(", high, ",", low, ")")

    highlow(high, low + 1)
}

func main() {
    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

Di seguito è riportato l'output generato:

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
panic: highlow() low greater than high

goroutine 1 [running]:
main.highlow(0x2, 0x3)
	/tmp/sandbox/prog.go:13 +0x34c
main.highlow(0x2, 0x2)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x1)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x0)
	/tmp/sandbox/prog.go:18 +0x298
main.main()
	/tmp/sandbox/prog.go:6 +0x37

Program exited: status 2.

Ecco cosa accade quando viene eseguito il codice:

  1. Tutto funziona normalmente. Il programma stampa i valori alti e bassi passati nella funzione highlow().

  2. Quando il valore di low è maggiore del valore di high, per il programma viene attivata la funzione panic. Viene visualizzato il messaggio Panic!. A questo punto, il flusso di controllo viene interrotto e tutte le funzioni posticipate iniziano a stampare il messaggio Deferred....

  3. Il programma si arresta in modo anomalo e viene visualizzata l'analisi dello stack completa. Non viene visualizzato il messaggio Program finished successfully!.

Si esegue in genere una chiamata alla funzione panic() quando non sono previsti errori gravi. Per evitare un arresto anomalo del programma, è possibile usare un'altra funzione denominata recover().

Funzione recover

In alcuni casi potrebbe essere necessario evitare un arresto anomalo del programma e segnalare invece l'errore internamente. Oppure, potrebbe essere preferibile risolvere i problemi prima che si verifichi un arresto anomalo del programma. È ad esempio possibile che sia necessario chiudere qualsiasi connessione a una risorsa per evitare ulteriori problemi.

Go offre la funzione predefinita recover() per consentire di riprendere il controllo dopo una funzione panic. La funzione recover deve essere chiamata solo in una funzione in cui si chiama anche defer. Se si chiama la funzione recover(), viene restituito nil senza altri effetti sulla normale esecuzione.

Provare a modificare la funzione main nel codice precedente per aggiungere una chiamata alla funzione recover(), come nell'esempio seguente:

func main() {
    defer func() {
	handler := recover()
        if handler != nil {
            fmt.Println("main(): recover", handler)
        }
    }()

    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

Quando si esegue il programma, l'output dovrebbe essere simile al seguente:

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
main(): recover from panic highlow() low greater than high

Program exited.

È chiara la differenza rispetto alla versione precedente? La differenza principale consiste nel fatto che non viene più visualizzato l'errore di analisi dello stack.

Nella funzione main() si posticipa una funzione anonima in cui viene chiamata la funzione recover(). Una chiamata a recover() non restituisce nil quando il programma è in panico. Sarebbe possibile intervenire per risolvere il problema, ma in questo esempio si sta semplicemente eseguendo una stampa.

Combinare le funzioni panic e recover è la soluzione usata abitualmente da Go per gestire le eccezioni. Altri linguaggi di programmazione usano il blocco try/catch. Go preferisce l'approccio qui descritto.

Per altre informazioni, vedere la proposta per aggiungere una funzione try predefinita in Go.