L'utilizzo dell'istruzione di salto incondizionato goto è solitamente considerato indice di cattiva programmazione. Il motivo è che l'uso (o meglio, abuso) del goto spesso rende il programma illeggibile e contorto (in questo caso si parla di "spaghetti code").

Ma è davvero un'istruzione da evitare ad ogni costo? Di seguito cercherò di mostrare come, in realtà, in alcune situazioni l'istruzione possa risultare non solo utile ma anche più "leggibile": il discorso che farò è valido per il C mentre non lo è per i linguaggi di più alto livello (penso a Java, C++, C#, Python...), principalmente perchè, come vedremo, questi forniscono una gestione delle eccezioni (anche se, in fondo, un'eccezione non è forse un goto messo in bella forma?).

spaghetti 540 360

Le situazioni in cui goto può risultare utile, almeno in linguaggio C, sono:
- uscita da cicli nidificati
- gestione semplificata del cleanup di una funzione
- ottimizzazione a basso livello.

Questo elenco deriva non solo dalla mia piccola esperienza personale ma anche dai risultati delle varie discussioni seguite su Internet.
Qui non parlerò dell'ottimizzazione a basso livello perchè sarebbe difficile portare un esempio: in questo caso mi è capitato di dover utilizzare il goto nello sviluppo di firmware, specie in alcune routine di gestione interrupt, dove era più importante il codice generato che la leggibilità del sorgente (sta di fatto che queste routine devono essere adeguatamente spiegate tramite un commento che ne illustri il funzionamento!).

USCIRE DA CICLI NIDIFICATI

Trovandoci di fronte ad una serie di cicli nidificati e ad una condizione che impone l'uscita siamo costretti ad utilizzare una flag, in quanto l'istruzione di break fa uscire solo dal ciclo in cui ci troviamo.

Ecco un esempio:

/*-----------------------------------*/
void FuncNestedLoopNoGoto(void)
{
    int i,j,k;
    int end = 0;

    for( i=2;i<1000;i++)
    {
        for(j=2;j<1000;j++)
        {
            for(k=2;k<1000;k++)
            {
                if( ( j == (i*i) ) && ( k == (j*j) ) )
                {
                    end = 1;
                    break;
                }
            }
            if( end )
                break;
        }
        if( end )
            break;
    }
    printf("%d %d %d\n", i, j, k);
}

L'utilizzo del goto potrebbe risultare utile per evitare il codice aggiuntivo che in ogni ciclo testa la variabile di uscita (end nel nostro esempio).

La stessa funzione con l'utilizzo del goto potrebbe essere:

/*-----------------------------------*/
void FuncNestedLoopGoto(void)
{
    int i,j,k;
    for( i=2;i<1000;i++)
    {
        for(j=2;j<1000;j++)
        {
            for(k=2;k<1000;k++)
            {
                if( ( j == (i*i) ) && ( k == (j*j) ) )
                    goto endloop;
            }
        }
    }
endloop:
    printf("%d %d %d\n", i, j, k);
}

Ammetto che tendo sempre a preferire la soluzione con la flag: ci sono però delle situazioni dove la gestione della flag allunga, o meglio appesantisce, una funzione già complessa e quindi in quei casi può risultare più chiaro uscire deliberatamente con un goto. In fondo, il senso del goto in questo caso è molto chiaro...

Linguaggi a più alto livello forniscono delle istruzioni come le labeled break di Java (vedi https://docs.oracle.com/javase/tutorial/java/nutsandbolts/branch.html) ma... non somigliano forse ad un goto?

Genericamente, con un linguaggio che supporta le eccezioni è possibile sostituire il goto con il raise di un'eccezione ma... questo non è peggio di un goto?

CLEANUP DI UNA FUNZIONE

Spesso capita di dover allocare diverse risorse (memoria, file, ...) all'interno della stessa funzione e quindi, naturalmente, gestirne la deallocazione/chiusura all'uscita. Il problema sorge nel momento in cui qualcosa va storto e ci troviamo nella situazione che alcune risorse sono allocate/aperte e altre no: bisogna scrivere codice apposito in ciascun punto di uscita.

Ecco un esempio che alloca 3 stringhe:

/*-----------------------------------*/
void FuncResourceNoGoto(void)
{
    char *pstr1;
    char *pstr2;
    char *pstr3;

    pstr1 = (char *)malloc(40);
    if( pstr1 == NULL )
    {
        return;
    }

    pstr2 = (char *)malloc(40);
    if( pstr2 == NULL )
    {
        free( pstr1 );
        return;
    }

    pstr3 = (char *)malloc(40);
    if( pstr3 == NULL )
    {
        free( pstr2 );
        free( pstr1 );
        return;
    }

    /* ... */

    free( pstr1 );
    free( pstr2 );
    free( pstr3 );
}

C'è chi potrebbe essere portato a pensare che una malloc() non possa mai fallire, specie con così pochi caratteri, e quindi non testare il valore ritornato da malloc(); io trovo che sia sempre meglio mettere le mani avanti, anche perchè non si sa mai quale evoluzione avrà la funzione e su quali sistemi verrà fatta lavorare.

La cosa peggiore che una persona possa fare è ignorare o coprire un problema
Masaaki Imai (guru della qualità)

Ritornando al discorso del goto: la funzione è piuttosto semplice ma cosa succede se dobbiamo aggiungere la gestione di un file? Dobbiamo aggiungere la fopen(), la fclose() alla fine e soprattutto dobbiamo gestire l'eventuale errore di apertura in modo da deallocare correttamente le 3 stringhe.

Se prevediamo che la funzione possa evolvere possiamo adottare uno schema che preveda, in caso di errore, il goto al punto di uscita dalla funzione, chiamamolo cleanup, come nell'esempio che segue:

/*-----------------------------------*/
void FuncResourceGoto(void)
{
    char *pstr1 = NULL;
    char *pstr2 = NULL;
    char *pstr3 = NULL;

    pstr1 = (char *)malloc(40);
    if( pstr1 == NULL )
        goto cleanup;

    pstr2 = (char *)malloc(40);
    if( pstr2 == NULL )
        goto cleanup;

    pstr3 = (char *)malloc(40);
    if( pstr3 == NULL )
        goto cleanup;

    /* ... */

cleanup:
    if( pstr1 )
        free( pstr1 );
    if( pstr2 )
        free( pstr2 );
    if( pstr3 )
        free( pstr3 );
}

Prima di tutto bisogna notare che le risorse vengono inizializzate a NULL in modo da poter capire, nel cleanup, se la risorsa è da deallocare oppure no, senza ulteriori flag.

Poi c'è da notare che, praticamente, lo schema ripropone una banalissima gestione delle eccezioni: infatti in linguaggi a più alto livello queste situazioni possono essere gestite con try/catch/finally.

Rimanendo nell'esempio, in linguaggio C: ora se vogliamo aggiungere la gestione di un file dobbiamo solo aggiungere la fopen() (come prima), la fclose() alla fine (come prima) e il controllo di errore di apertura del file può semplicemente (a differenza di prima) essere un goto al cleanup della funzione.

In questo caso l'uso del goto porta ad una migliore leggibilità e, soprattutto, una maggiore facilità di manutenzione.

FONTI

A questo indirizzo http://tekkyno.altervista.org/blog/tag/goto/ trovate riportata un'esperienza con il kernel di Linux, con l'utilizzo dei goto per la gestione degli errori e del rilascio delle risorse.

In questo articolo http://www.drdobbs.com/jvm/programming-with-reason-why-is-goto-bad/228200966 si dimostra come certe pratiche di programmazione siano da considerare alla stregua di un goto: ad esempio, avere diversi punti di uscita con return all'interno di una funzione potrebbe essere considerato come un "goto" in bella forma.
Allo stesso modo il break e il continue possono risultare poco "leggibili", come un goto, specie se all'interno del ciclo ne vengono inseriti in diversi punti.

Infine, in questa discussione http://stackoverflow.com/questions/24451/is-it-ever-advantageous-to-use-goto-in-a-language-that-supports-loops-and-func si trovano innumerevoli spunti di riflessione.

CONCLUSIONE

Con questo articolo non voglio convincere nessuno ad utilizzare goto; il mio intento era di dimostrare come anche il tanto odiato goto possa in realtà essere un valido strumento per ottenere codice agile, leggibile e semplice da manutenere.

A quanti affermano che il goto è dannoso rispondo che l'istruzione non è dannosa, eventualmente lo è l'uso che se fa; anche un martello può essere dannoso, se ci si pesta il dito, ma non per questo diciamo che il martello non bisogna utilizzarlo!
La leggibilità di un programma, argomento principale dei detrattori del goto, può essere penalizzata anche da una serie di "break" o "continue" o un ciclo scritto male: la leggibilità dipende solo da chi scrive!