Go - from Google


Errori ed eccezioni     

La gestione degli errori a runtime è questione fondamentale per ogni linguaggio di programmazione e non è argomento semplice visto che può riguardare tanto sottili bug dovuti a sbagli in fase di codifica quanto disastri totali determinati ad esempio da problematiche hardware. Le famose sequenze try - catch - finally si trovano, in varie salse, un po' ovunque. Premettiamo che l'argomento è molto complesso e in questa sede toccheremo i rudimenti di base cercando di restare ad un livello molto semplice. Più avanti approfondiremo il discorso fermo restando che chi ha già esperienza nel campo non farà fatica ad allargare il discorso con le proprie competenze. Il linguaggio Go è stato un po' restio ad accettare meccanismi comuni e normali ad esempio per chi usa Java o C#, in particolare quando si parlava di eccezioni. Le release iniziali non prevedevano keywords dedicate e, per quanto mi ricordo, non c'era entusiasmo da parte degli autori relativamente alla loro futura adozione mantenendo invece salda l'idea di poter rendere il linguaggio più leggero possibile. La comunità tuttavia spingeva in maniera abbastanza convinta in quella direzione ed eccoci arrivati all'argomento di una parte di questo paragrafo. Le eccezioni ed il loro trattamento sono argomento serio ed impegnativo che non possono prescindere dall'esperienza, che, tra l'altro, insegna spesso a non abusarne, qui daremo le nozioni di base dopo aver parlato degli errori meno "gravi". Da qualche parte, putroppo non ricordo dove, ho letto queste parole che sintetizzano perfettamente la situazione: "non è che in Go manchino i meccanismi semanticamente equivalenti al classico trattamento delle eccezioni di altri linguaggi; è diversa la cultura dell'utilizzare questo meccanismo per controllare il flusso del programma".
Da un punto di vista generale i meccanismi "pesanti" equivalenti appunto alle sequenza try - catch di C# o Java sono intesi nelle intenzioni degli estensori come ultima risorsa, da usare in modo estremamente parco;  Pike e soci ritengono che la via standard sia quella di usare i valori di ritorno delle funzioni e comunque tutto il meccanismo relativo alle eccezioni è stato concepito in modo da limitare al massimo l'uso di risorse. Si tratta quindi di una gestione "leggera" il che non è poi una cosa negativa, in se. Per sgombrare il campo da equivoci diciamo subito che Go, ovviamente, non si sogna minimamente di ignorare gli errori più comuni, ci mancherebbe, e vedremo qui come si usa affrontarli con strumenti noti. Comunque, a maggior conferma di quanto detto, Go ha una interfaccia predefinita per la gestione degli errori:

type error interface {
Error() string
}

Per quanto concerne invece il trattamento di eventi eccezionali abbiamo due paroline magiche che reggono il tutto: panic e recover. Questi costrutti potrebbero anche essere usati in forma generalizzata ma in Go questo è considerato un cattivo stile di programmazione ed è fortemente scoraggiato. La stessa parola "panic" in fondo ci avvisa che siamo di fronte a qualche evento davvero eccezionale e, forse, non recuperabile. D'altra parte invece recover lascia aperta la strada nel caso in cui qualche cosa si possa tentare. E' considerata cosa buona, chiamiamola una best practice, usare recover per bloccare un panic. Come accennato sono concettualmente equivalenti alle classiche coppie try + catch che troviamo in altri linguaggi (C#, Java ecc...).  Vediamo una sia pur inutile applicazioni che dimostra una chiamata a panic e cosa succede quando esso interviene. Ovviamente è solo un modo per presentare la keyword:

  Esempio 13.1
1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
fmt.Println("Inizio")
panic("Orrore")
fmt.Println("fine")
}

L'esecuzione vi farà capire qualche cosa del funzionamento di panic. Ma. ovviamente, non è un esempio interessante.
In generale comunque l'intervento di panic dà il via ad una serie di operazioni:

  • Il metodo (o funzione, di seguito useremo la parola metodo) che ha provocato lo stato di panic viene immediatamente fermato (nell'esempio precedente essendo il main il metodo in questione il programma è terminato)
  • Ogni eventuale metodo deferred viene eseguito come se il metodo precedente fosse terminato normalmente
  • il controllo è restituito al chiamante passandogli lo stato di panic generato dal metodo chiamato
  • il processo si ripete: vengono eseguiti i metodi deferred e si sale di livello fino ad arrivare al main
  • il programma termina
Il procedimento iterativo appena visto può avere un andamento diverso se una delle funzioni deferred contiene al suo interno delle istruzioni di recover ; in questo caso la risalita del panic si arresta e possono essere messe in pratica delle azioni di recupero o, se è il caso, si può anche ignorare il panic (di norma questa pratica è sconsigliata). Va da se che recover ha senso solo in un metodo deferred.

Per vedere in azione i meccanismi appena puntualmente descritti cediamo di costruire un esempio, puramente didattico, e lo faremo per gradi senza dar peso ad una situazione specifica che può aver originato la situazione di panico.

  Esempio 13.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func f1() {
fmt.Println("Panico!")
panic("appunto...")
}

func main() {
fmt.Println("Inizio")
f1()
fmt.Println("fine")
}

L'output è il seguente:

Inizio
Panico!
panic: appunto...

goroutine 1 [running]:
main.f()
C:/Go/bin/panrec02.go:7 +0xad
main.main()
C:/Go/bin/panrec02.go:12 +0x88

goroutine 2 [runnable]:

Come si vede la funzione f lancia il panico e rimanda il controllo al chiamante che termina per effetto della situazione creatasi. Ovviamente quel "panrec02.go" che compare è il nome che ho dato al programma di test. Modifichiamo ora il programma inserendo una funzione differita:

  Esempio 13.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func postpanic() {
fmt.Println("esecuzione differita")
}

func f() {
defer postpanic()
fmt.Println("Panico!")
panic("appunto...")
}

func main() {
fmt.Println("Inizio")
f()
fmt.Println("fine")
}

Il programma 13.3 ha questo output:

Inizio
Panico!
esecuzione differita
panic: appunto...

goroutine 1 [running]:
main.f()
C:/Go/bin/panrec03.go:12 +0xc3
main.main()
C:/Go/bin/panrec03.go:17 +0x88

goroutine 2 [runnable]:

Ho evidenziato la riga di output relativa alla funzione differita. A questo punto è ora di far entrare in scena il nostro recover:

  Esempio 13.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func postpanic() {
fmt.Println("esecuzione differita")
if r := recover(); r != nil {
fmt.Println("Operazione recuperata")
}
}

func f() {
defer postpanic()
fmt.Println("Panico!")
panic("appunto...")
}

func main() {
fmt.Println("Inizio")
f()
fmt.Println("fine")
}

Ed ecco l'output:

Inizio
Panico!
esecuzione differita
Operazione recuperata
fine

Ebbene, il panico non va in onda! Tutto merito della istruzione alla riga 7 dell'esempio 13.4. Vediamo come funziona la cosa e poi facciamo qualche doverosa considerazione perchè le cose non sono ovviamente così facili e banali; recover è una funzione built-in nel core del linguaggio che ha il preciso scopo di riprendere il controllo di un metodo che abbia generato un evento di panico. Essa viene ricavata ed assegnata ad una variabile, convenzionalmente chiamata r in molti esempi. Panic e recover sono definiti internamente come segue:

func panic(interface{})
func recover() interface{}


e in particolare recover, per chiarire ulteriormente la riga 7 del 13.4, può assumere valore nil in 3 casi:

1) se l'argomento di panic è nil
2) se la routine non è in panico
3) se recover non è inserita in un metodo deferred

Naturalmente va considerato che recover deve gestire la specifica situazione di errore. Nell'esempio precedente siamo rimasti molto sul generico giusto per far vedere per sommi capi il funzionamento; però lo spirito di recover è quello di essere strettamente legato ad una situazione di panic e di proporre un modo di uscita coerente.... non ha senso gestire, che so, una divisione per zero stampando a video una poesia del Carducci. In questo senso solo l'esperienza può fare da guida pratica.