Il supporto per i programmi simultanei (concurrent programming) in Go è un aspetto nuovo e molto interessante. Vediamo un esempio semplice e pratico

CONCURRENT, GOROUTINE E CHANNELS

Una caratteristica peculiare e distintiva di Go è il supporto nativo per la scrittura di programmi concurrent, con goroutine e channels.

APPLICAZIONE SEQUENZIALE

In un programma classico sequenziale, il tempo impiegato per l’esecuzione totale del processo è pari alla somma di tutte le funzioni che l’applicazione dovrà eseguire uno in seguito all’altra.

Per capire meglio la differenza che intercorre tra un programma lineare e uno concurrent riscriviamo, come esempio, un programma simile a curl: inserendo un indirizzo URL stampa a video il testo della pagina web. Logicamente il codice può essere ampliato con l’aggiunta di numerose altre opzioni, ma è volutamente basico.

CODICE

package main

import (
	"fmt"
	"io/ioutil"
	"net/http" 
	"os"
)

func main() {
	for _, url := range os.Args[1:] { // Lettura URL che inseriamo nella riga di comando
		risp, err := http.Get(url) // Esecuzione richiesta
		if err != nil { // Controllo se la richiesta ha restituito un errore
			fmt.Fprintf(os.Stderr, "Errore richiesta URL: %v\n", err) // Stampa l'errore 
			os.Exit(1) // Uscita dal programma
		}
		b, err := ioutil.ReadAll(risp.Body) // Lettura contenuto della pagina web
		risp.Body.Close() 
		if err != nil { // Controllo se la lettura della pagina ha restituito un errore
			fmt.Fprintf(os.Stderr, "Errore in lettura %s: %v\n", url, err) // Stampa l'errore
			os.Exit(1) // Uscita dal programma
		}
		fmt.Printf("%s", b) //Stampa il contenuto della pagina
	}
}

FUNZIONAMENTO

Il funzionamento di questo programma è semplice: la funzione http.Get esegue la richiesta HTTP e, se non ci sono errori, ritorna la risposta nella struttura risp. Nel campo Body di risp è contenuta la risposta in un formato tale da poter essere letto e interpretato. Successivamente, ioutil.Readall legge la risposta per intero e la salva nella variabile b. Lo stream di Body è chiuso per evitare di utilizzare troppe risorse inutilmente e, per finire, viene stampato a video con Printf la risposta in un output standard.

1
2
3
4
5
$ go build sequenziale.go
$ ./sequenziale https://www.google.it
<html>
<head>
<title>Google</title>

Mentre se la richiesta HTTP fallisce, otterremmo questo risultato:

1
2
$ ./sequenziale https://errore.google.it
Errore richiesta URL: Get https://errore.google.it: dial tcp: lookup errore.google.it: no such host

In entrambi i casi di errore, os.Exit(1) farà chiudere il programma con un codice di errore pari a 1.

CODICE PER FUNZIONI SIMULTANEE

Il prossimo codice che propongo, recupera simultaneamente degli URLs in modo tale che il processo impiegherà come tempo massimo la risposta più lunga degli URLs.
Sono omessi intenzionalmente controlli vari per rendere il codice più leggibile.

CODICE

package main

import (
    "fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

func main() {
	inizio := time.Now()
	ch := make(chan string)
	for _, url := range os.Args[1:] {
		go funzioneRecupero(url, ch) // inizio di a goroutine
	}
	for range os.Args[1:] {
		fmt.Println(<-ch) // ricevo da channel ch
	}
	fmt.Printf("%.2fs passato\n", time.Since(inizio).Seconds())
}

func funzioneRecupero(url string, ch chan<- string) {
	inizio := time.Now()
	resp, err := http.Get(url)
	if err != nil {
		ch <- fmt.Sprint(err) // invio a channel ch
		return
	}

	nbytes, err := io.Copy(ioutil.Discard, resp.Body)
	resp.Body.Close() // liberiamo le risorse
	if err != nil {
		ch <- fmt.Sprintf("mentre leggo %s: %v", url, err)
		return
	}
	secs := time.Since(inizio).Seconds()
	ch <- fmt.Sprintf("%.2fs  %7d  %s", secondi, nbytes, url)
}

L’output sarà il seguente

1
2
3
4
5
$ go build simultaneo.go
$ ./simultaneo https://www.google.it https://www.facebook.com https://www.twitter.com
0.15s   5442   https://www.google.it 
0.16s   6335   https://www.twitter.com
0.17s   7455   https://www.facebook.com

FUNZIONAMENTO

Una goroutine è una funzione eseguita simultaneamente. Un channel è un meccanismo di comunicazione che permette a una goroutine il passaggio di un valore specifico a un’altra goroutine. la funzione main è eseguita in una goroutine e la parola go crea ulteriori goroutines.

La funzione main crea un channel di testo usando make. Per ogni argomento della command-line, l’istruzione go nel primo loop inizia una nuova goroutine che richiama funzioneRecupero in modo asincrono per recuperare l’URL attraverso http.Get. La funzione io.Copy legge il contenuto della risposta inviandolo a ioutil.Discard. Copy ritorna sia il conteggio dei byte sia qualsiasi errore, se è presente. Per ogni risultato, la funzione funzioneRecupero invia i dati a ch, il channel. Il secondo loop in main lo riceve e stampa i dati.

Quando una goroutine tenta di inviare o ricevere su un channel, si blocca fino a quando un’altra goroutine tenta una operazione di ricezione o invio e in quel determinato momento il valore è trasferito ed entrambe le goroutine continuano il loro lavoro. Nel secondo esempio, ogni funzioneRecupero invia un valore (ch <- espressione) al channel ch, e main le riceve tutte (<- ch). In main il comando di print assicura che l’output di ogni goroutine è processata singolarmente, evitando problemi se due goroutines finiscono nello stesso istante.