Upload
vanxuyen
View
219
Download
0
Embed Size (px)
Citation preview
Scuola Politecnica e delle Scienze di Base Corso di Laurea in Ingegneria Informatica
Elaborato finale in Programmazione I
Il linguaggio di programmazione Go di Google
Anno Accademico 2016/17
Candidato: Domenico Iannucci matr. N46/2034
A Michele, mio padre.
Indice
Indice .................................................................................................................................................. III
Introduzione al linguaggio Go ............................................................................................................. 4
I. Motivazioni nel panorama attuale dello sviluppo software ......................................................... 4
II. Obiettivi del linguaggio ............................................................................................................... 5
III. Caratteristiche fondamentali ...................................................................................................... 5
IV. Storia del linguaggio .................................................................................................................. 6
Capitolo 1: Sintassi del linguaggio ...................................................................................................... 8
1.1 Formattazione............................................................................................................................. 8
1.2 Nomi, dichiarazioni e assegnazioni ........................................................................................... 8
1.3 Tipi di dato ................................................................................................................................. 9
1.3.1 Allocazione con new e make ............................................................................................. 10
1.4 Strutture di controllo ................................................................................................................ 10
1.5 Funzioni e defer ....................................................................................................................... 12
1.6 Metodi e interfacce ................................................................................................................... 12
1.7 Goroutines e channels .............................................................................................................. 13
Capitolo 2: Compilazione ed esecuzione ........................................................................................... 15
2.1 Organizzazione in package e dipendenze ................................................................................ 15
2.2 Compilazione ed esecuzione .................................................................................................... 16
2.3 Errori, panic e recover ............................................................................................................. 17
Capitolo 3: Aspetti avanzati ............................................................................................................... 18
3.1 Modello di allocazione delle variabili e garbage collector ..................................................... 18
3.2 Polimorfismo ............................................................................................................................ 18
3.3 Gestione dell’I/O ...................................................................................................................... 19
3.4 Gestione della concorrenza e parallelizzazione ....................................................................... 20
3.5 Cenni a rete, socket e design patterns ...................................................................................... 21
Capitolo 4: Confronto con C++ e Java .............................................................................................. 22
Capitolo 5: Applicazioni esistenti ...................................................................................................... 23
Capitolo 6: Sviluppo di un programma in Go .................................................................................... 24
6.1 Installazione e ambienti di sviluppo ......................................................................................... 24
6.2 Esempio di programma in Go .................................................................................................. 24
Conclusioni ........................................................................................................................................ 29
Bibliografia ........................................................................................................................................ 30
Appendice .......................................................................................................................................... 32
4
Introduzione al linguaggio Go
In questo elaborato verrà analizzato il linguaggio di programmazione Go sviluppato da
Google. Nell’introduzione saranno descritte le motivazioni che hanno portato allo sviluppo
del progetto Go, gli obiettivi che i creatori si sono posti, le caratteristiche fondamentali che
rendono questo linguaggio di programmazione peculiare e una breve storia di Go, con una
panoramica delle versioni. A seguire nel capitolo 1 sarà esposta la sintassi del linguaggio
mentre nel capitolo 2 la compilazione e l’esecuzione di un programma Go con attenzione
anche alla gestione dell’errore. Ancora, nel capitolo 3 verranno chiariti gli aspetti avanzati
del linguaggio, in particolare si farà riferimento alla gestione della concorrenza, e nel
capitolo 4 i linguaggi Go, C++ e Java saranno messi a confronto sulla base di alcune
caratteristiche. A conclusione, il capitolo 5 illustrerà l’impiego attuale del linguaggio Go
nello sviluppo di applicazioni industriali e il capitolo 6 presenterà un esempio di sviluppo
di un programma in Go attraverso un semplice caso di studio che comunque ha consentito
di applicare alcuni concetti di ingegneria del software, sistemi operativi e programmazione.
I. Motivazioni nel panorama attuale dello sviluppo software
L’esigenza di creare un nuovo linguaggio di programmazione sorge dai problemi incontrati
dagli ingegneri Google durante lo sviluppo delle infrastrutture software dell’azienda e dalla
loro analisi delle sfide oggi poste dallo sviluppo software. Nell’ultimo decennio infatti, al
progresso nello sviluppo di sistemi hardware, avutosi nello specifico con l’introduzione dei
processori multicore, non è corrisposto un analogo progresso nello sviluppo dei sistemi
software. Anzi, la gestione della dimensione degli attuali progetti è diventata critica, in
particolare la gestione delle dipendenze tra moduli nello sviluppo software determina
5
sempre più la necessità di strumenti di compilazione veloce e di strumenti per una
efficiente analisi delle dipendenze. I programmatori stanno tendendo verso l’uso di
linguaggi tipizzati dinamicamente, che facilitano la programmazione, come Python o
Javascript, che supportano alcuni meccanismi in grado di ottimizzare le prestazioni delle
applicazioni da un lato e del sistema dall’altro, quali il garbage collector e i meccanismi
per la gestione della concorrenza, che consentono di sfruttare la potenza di calcolo dei
processori multicore. Tali meccanismi non sono ben supportati dai linguaggi di
programmazione tradizionali e la necessità di un nuovo linguaggio di programmazione
nasce quindi dall’esigenza di avere un linguaggio che risponda ai seguenti requisiti:
compilazione efficiente, esecuzione veloce e facilità di programmazione.
II. Obiettivi del linguaggio
Il principale obiettivo è creare un linguaggio multipurpose, adatto quindi a diversi scopi e
ambienti di sviluppo, che coniughi la velocità di compilazione all’esecuzione efficiente e
alla facilità di programmazione. Il proposito è combinare l’efficienza e la safety dei
linguaggi compilati e tipizzati staticamente con la facilità di programmazione dei linguaggi
interpretati e tipizzati dinamicamente. L’idea è stata di renderlo più produttivo rispetto a
linguaggi come C++, Java e Python, i quali sono stati sviluppati in anni in cui gli ambienti
e le problematiche di sviluppo erano totalmente diverse. Go, quindi, nativamente introduce
supporto ai processori multicore e alle reti, oltre alla costruzione di un programma
flessibile e modulare. Lo scopo, non ancora realizzato in forma matura, è ridurre il
problema della scalabilità: oggi i web server hanno decine di milioni di righe di codice, con
conseguente fase di compilazione della durata di minuti, se non di ore. Go, invece,
promette una compilazione efficiente e di breve durata, persino sulle macchine meno
performanti. Per raggiungere questo obiettivo, Go ha dovuto affrontare una serie di
questioni linguistiche, con conseguenti scelte non immediatamente intuitive.
III. Caratteristiche fondamentali
Il Go è un progetto open-source in licenza BSD. Esistono quindi dei repository pubblici
importati da Google. È un linguaggio compilato, caratterizzato da un’alta velocità di
6
esecuzione e di compilazione. Quest’ultima è eccellente sia su macchina singola che per
grossi file sorgente. Può essere definito semplice, perché è simile a livello sintattico al C
ma molto ottimizzato. Mantiene quindi le caratteristiche del C, aggiungendone altre tipiche
dei linguaggi dinamici. Implementa inoltre a livello nativo la concorrenza tramite le
goroutines, cioè funzioni eseguite concorrentemente anche su processori multicore.
Avendo un sistema di tipizzazione di tipo non gerarchico, garantisce la sicurezza. Il
linguaggio è strettamente controllato nell’uso della memoria e nella tipizzazione, a
differenza del C. In Go, sempre per garantire la sicurezza, è stata esclusa l’aritmetica dei
puntatori. Il linguaggio è corredato di garbage collector, che lo rende più semplice, poiché
il programmatore è sollevato dalla gestione della memoria. In sintesi, gli ingegneri di
Mountain View hanno preso ispirazione dai problemi dei principali linguaggi di
programmazione: la difficoltà per il C, la lentezza dei linguaggi interpretati e dai margini
legati alle performance delle macchine virtuali.
IV. Storia del linguaggio
L’idea nasce nel 2007 da Robert Griesemer, Rob Pike, e Ken Thompson per cercare di
risolvere alcuni problemi che si presentavano durante lo sviluppo delle infrastrutture
software di Google. “Go è un linguaggio di programmazione sviluppato da Google per
aiutare a risolvere i problemi di Google, e Google ha
grandi problemi.” [1] Il nome Go è stato deciso dopo una
lunga fase di sviluppo, e non è stato esente da
controversie. Nel panorama mondiale, esisteva già un
linguaggio dal nome Go!, ma nonostante la richiesta del
suo creatore Francis McCabe, il nome del linguaggio non
è stato modificato poiché, secondo Google, i due
linguaggi non possono essere confusi. La prima versione
stabile del linguaggio è del marzo 2011 e solo un anno dopo è stata rilasciata la versione
1.0 che ne ha sancito ufficialmente la nascita. Il linguaggio, negli anni, grazie all’influenza
di BigG è riuscito a ottenere sempre più popolarità, aggiudicandosi il titolo di “Linguaggio
di programmazione dell’anno” di TIOBE nell’anno 2009 e nell’anno 2016. [2]
Gopher: la mascotte di Go
7
Nel seguito verrà indicata brevemente una panoramica delle major releases con le novità
introdotte. [3]
La novità della versione 1.0 è l’introduzione dell’interfaccia error contenente il metodo
Error(). Questa è fondamentale poiché apre il linguaggio alla gestione degli errori,
inizialmente assente. Altra introduzione sono gli operatori == e != per le struct e gli array.
Tali operatori vengono ora definiti, non in base ai puntatori, ma ai campi e agli elementi,
come è più solito l’utilizzo da parte degli utenti.
Con la versione 1.1, del maggio 2013, viene trattata la divisione intera per zero come un
errore a tempo di compilazione e non più a run-time.
La versione 1.2, di dicembre 2013, vede l’introduzione di un limite configurabile del
numero di threads che può avere un programma per evitare la starvation delle risorse.
Inoltre, viene incrementato lo stack a 8KB delle goroutines.
Lo stack delle goroutines è stato modificato nella versione 1.3 del giugno 2014, ove si è
passato da un modello di memoria segmentato a uno contiguo. Se una goroutine necessita
di più memoria stack di quella effettivamente disponibile, l’intero stack viene trasferito su
una porzione di memoria maggiore.
Nel dicembre 2014 è rilasciata la versione 1.4, con la quale viene modificata la sintassi del
ciclo for per evitare di definire un indice se non è effettivamente usato. Viene inoltre
aggiunto il supporto ai processori ARM per compilare file binari per il sistema operativo
Android.
Caratteristica fondamentale della versione 1.5 di agosto 2015 è la totale assenza del
linguaggio C. Da questa implementazione il compilatore è scritto totalmente in Go e in
assembler. Il garbage collector ora è concorrente con le goroutines.
Dalla versione 1.6 di febbraio 2016 alla versione 1.9 di agosto 2017, vengono incrementate
le performace del garbage collector, viene migliorata la gestione della concorrenza e
vengono aggiunti vari tools di supporto.
Nel luglio 2017 si è iniziato a parlare di Go 2 che porterà futuri miglioramenti e il cui
obiettivo sarà risolvere i problemi di scalabilità incontrati da Go 1.
8
Capitolo 1: Sintassi del linguaggio
In questo capitolo verrà presentata la sintassi principale del linguaggio di programmazione
Go, consultabile sulla documentazione ufficiale, in relazione al C/C++. [4][5]
1.1 Formattazione
Il programma gofmt analizza il codice sorgente e lo riformatta nella maniera corretta. Nel
particolare segue un insieme di regole, quali l’indentazione e il giusto allineamento delle
colonne e dei commenti. Ciò permette una facile leggibilità dei sorgenti da parte di diversi
programmatori, poiché vengono adottate regole standard.
I punti e virgole non appaiono come in C/C++ alla fine di ogni riga di codice.
L’analizzatore lessicale del linguaggio Go provvede automaticamente all’inserimento di
questi dove necessario, cioè al seguito di alcuni simboli e parole chiave.
1.2 Nomi, dichiarazioni e assegnazioni
Per utilizzare i contenuti di un package importato nel sorgente, si usa la dot notation. Ad
esempio, per stampare a video si usa la funzione Println del package fmt:
fmt.Println(“Hello world!”)
Il linguaggio Go è case-sensitive, e in aggiunta attribuisce un particolare significato alle
lettere maiuscole. Nel particolare, i campi di una struttura hanno visibilità pubblica solo se
il nome inizia per lettera maiuscola, in caso contrario la visibilità è privata. La convenzione
utilizzata stabilisce una regola anche per i metodi set e get. Per il primo viene usato
SetVariabile(), invece per le get si conviene usare Variabile() in luogo di GetVariabile().
9
Per il nome delle interfacce, nel caso contengano un solo metodo, è prevista l’aggiunta del
suffisso -er al nome del metodo contenuto. Ad esempio, un’interfaccia con un metodo
Write() sarà nominata Writer().
Per definire le variabili, il linguaggio offre le seguenti parole chiave: var per le variabili, il
cui valore può essere modificato nel corso del programma, e const per le costanti.
Go supporta l’inferenza di tipo, cioè la capacità del compilatore di dedurre il tipo di
variabile o costante in base al valore. Sono ammissibili, quindi, dichiarazioni sulla base di
assegnazioni, come ad esempio:
var a := 10
var b int
c := 5
1.3 Tipi di dato
Nel linguaggio Go esistono i seguenti tipi base di dato:
- numeri: int8, int16, int32, int64, e le corrispondenti versioni uint, float32, float34,
complex64, complex128
- byte, è un alias per uint8
- bool per i valori booleani
- per le stringhe string
Su questi tipi di dato sono definiti i consueti operatori.
I tipi di dati strutturati nel linguaggio Go, invece, sono le struct, gli array, le slices e le
maps.
Le strutture sono tipi valore che contengono uno o più campi di diverso tipo. Questi campi
possono essere acceduti dall’esterno del package di appartenenza se hanno nomi inizianti
per lettera maiuscola, in caso contrario sono visibili solo all’interno del medesimo
package.
Gli array in Go sono sequenze di elementi, dello stesso tipo base, di lunghezza fissa. La
principale differenza rispetto al C/C++ è che questi sono valori, cioè assegnare un array ad
un altro significa copiare tutti gli elementi. E ancora, passare un array a una funzione
significa che questa ne riceverà una copia.
10
Le slices sono un reference type, nel particolare un riferimento a una porzione di array, che
può anche coincidere con l’array stesso. La lunghezza delle slices è modificabile
dinamicamente, però questa non può mai eccedere la propria capacità, che è data dal limite
dell’array sottostante.
Un tipo map è una sequenza di coppie chiave-valore, aventi il vincolo dell’unicità della
chiave.
1.3.1 Allocazione con new e make
Le primitive di allocazione in Go sono new e make, le quali presentano alcune differenze.
La funzione new alloca la memoria ma non la inizializza, semplicemente la azzera, e
ritorna il puntatore al tipo di dato allocato. Questo tipo di allocazione è utile quando non si
ha bisogno di ulteriori inizializzazioni. L’allocazione mediante make crea solo slices,
mappe e channels, e ritorna un valore di tipo inizializzato, non un puntatore. Il supporto a
solo tre tipi di dato è dovuto al fatto che rappresentano riferimenti a strutture dati che
devono essere inizializzate prima dell’uso.
1.4 Strutture di controllo
In Go non esistono cicli while-do e do-while presenti in altri linguaggi di programmazione.
A differenza di C/C++ non è previsto l’utilizzo delle parentesi tonde per la condizione da
verificare nella struttura di selezione if e if/else. Una novità introdotta in Go è la possibilità
di dichiarare e assegnare una variabile dopo la parola chiave if e controllare su questa la
condizione. Ad esempio:
if err := file.Chmod(0664) ; err != nil {
//altre istruzioni
}
La struttura di selezione switch-case, in Go, è stata notevolmente ampliata. Nella sua
struttura standard, la variabile da valutare può essere di qualsiasi tipo. È permesso inserire
più istruzioni nel medesimo case anche senza inserire parentesi graffe, e prevedere più
alternative per ogni case separando i valori con una virgola. Interessante è la totale assenza
della parola chiave break, poiché il fallthrough è possibile solo esplicitamente con
11
l’istruzione fallthrough, in modo da evitare l’errore di far eseguire più branch dello switch
in sequenza.
Sono state introdotti anche due utilizzi dello switch del tutto assenti in linguaggi come
C/C++. Il primo prevede che a livello dell’istruzione switch non sia presente alcuna
istruzione, lasciando ai vari case la valutazione di diverse condizioni.
switch {
case condizione1:
//istruzione
case condizione2:
//istruzione
default:
//istruzione
}
La seconda prevede una inizializzazione nella riga dello switch, conclusa da un punto e
virgola, e una serie di case con i valori da confrontare.
switch inizializzazione; {
case valore1:
istruzione
case valore2:
istruzione
default:
istruzione
}
In Go la struttura for comprende anche la struttura iterativa while, ma non il do-while,
poiché non previsto dal linguaggio.
La sintassi per il for è uguale al C/C++, ma con l’assenza delle parentesi tonde. È possibile
dichiarare la variabile indice nel for stesso, e prevedere più variabili indice, evitando, però,
l’uso degli operatori unari di incremento e decremento, poiché in Go non sono espressioni
ma istruzioni. Differisce, invece dal C/C++, nella possibilità di non dichiarare la variabile
indice: supponendo di voler scorrere un array mioarray per effettuare una somma degli
elementi, si può scrivere:
for _, valore := range mioarray {
somma+ = valore
}
Per quanto riguarda il ciclo while, invece, la sintassi è la seguente:
for condizione {
//istruzioni
}
12
1.5 Funzioni e defer
Un elemento distintivo del Go è la possibilità delle funzioni, ma anche dei metodi, di avere
valori di ritorno multipli. Ciò nasce per evitare il comportamento di alcune funzioni in C,
cioè far ritornare alla funzione un valore negativo per segnalare un errore. Ad esempio, la
funzione Read del package os restituisce due valori, come si legge dalla seguente firma:
func (f *File) Read(b []byte) (n int, err error)
In aggiunta, all’interno di una funzione è possibile utilizzare un parametro di ritorno senza
dichiararlo. Questo sarà automaticamente inizializzato a zero e, al termine della funzione,
restituito senza ulteriori specificazioni, tramite solo un return.
Utilizzando la dichiarazione defer prima di una chiamata a funzione, questa viene eseguita
immediatamente prima del return della funzione chiamante. Il classico esempio di utilizzo
è il rilascio di un mutex o la chiusura di un file. L’utilizzo del defer, nel caso del file, ha il
vantaggio di non far dimenticare ai programmatori la chiusura, e far apparire la chiusura
dopo l’apertura, che rende il codice più chiaro rispetto a chiudere il file alla fine della
funzione. Gli eventuali argomenti della funzione chiamata sono valutati all’esecuzione di
defer e non alla chiamata. In presenza di più defer, le funzioni vengono eseguite in ordine
LIFO prima del return.
1.6 Metodi e interfacce
Il linguaggio Go consente di definire metodi per qualsiasi tipo di dato, quindi non
necessariamente le strutture, ad eccezione di puntatori e interfacce. I metodi possono avere
come ricevente, cioè come tipo di dato su cui sono dichiarati, anche un puntatore al
ricevente. Nel caso del puntatore al ricevente verrà modificato il dato originale, in caso
contrario il metodo utilizza una copia del dato originale.
Le interfacce definiscono un comportamento di un certo tipo, essendo composte da un
insieme di metodi. Le interfacce sono come dei contratti fra l’aspettativa di
implementazione e la realizzazione effettiva. Il concetto di interfaccia, unitamente a quello
di struct, avvicina il linguaggio Go al paradigma di programmazione ad oggetti, poiché dal
13
punto di vista morfologico le classi sono delle strutture con dei metodi associati. Nella
sintassi del linguaggio, un tipo di dato non ha bisogno di esplicitare con parole chiave
l’implementazione di un’interfaccia ma deve solo implementare i metodi previsti da
questa. Naturalmente, più tipi possono implementare la stessa interfaccia e più interfacce
possono essere implementate da un solo tipo.
1.7 Goroutines e channels
Le goroutines sono il meccanismo adottato dal linguaggio Go per gestire il multithreading
nativamente. Il nome è stato scelto dai creatori poiché a loro parere genera meno
confusione di parole come threads e processi. Una goroutine è semplicemente una
funzione eseguita in concorrenza con altre goroutines nello stesso spazio di indirizzamento.
Le goroutines sono leggere e costano poco più dello spazio di allocazione di uno stack. Lo
stack in partenza è molto piccolo, 8KB, ma all’occorrenza viene ampliato liberando la
porzione di memoria e allocando un segmento di lunghezza maggiore. Le goroutines
vengono affidate a diversi threads del sistema operativo, cosicché, se una è in stato
bloccato in attesa ad esempio di un’operazione di I/O, le altre possono continuare
l’esecuzione. Per lanciare una goroutine basta anteporre la parola go alla chiamata di
funzione, ma bisogna notare che se il chiamante termina l’esecuzione allora vengono
terminate anche le goroutines chiamate.
I canali offrono un meccanismo per la comunicazione, inviando e ricevendo valori di un
tipo determinato in ordine FIFO, fra funzioni in esecuzione concorrente. Nella
dichiarazione di un canale, tramite la parola chan, è possibile specificare la direzione, cioè
invio o ricezione, con l’operatore <-. Ad esempio:
chan int // canale bidirezionale per valori interi
chan<- int // canale per l’invio di int
<-chan int // canale a sola ricezione di int
Un canale può essere creato con la funzione di allocazione make, che ha come parametri il
tipo di canale e opzionalmente la capacità:
make(chan float, 5)
14
La capacità rappresenta la lunghezza del buffer del canale. Se è zero o non specificata, il
canale è non bufferizzato, quindi il sender si blocca fino a quando il receiver non ha
ricevuto il valore; in caso contrario il canale è bufferizzato e il sender si blocca solo per
copiare il valore sul buffer, ma se questo è pieno significa aspettare la ricezione di un
receiver. Il canale può essere chiuso tramite la funzione close.
15
Capitolo 2: Compilazione ed esecuzione
In questo capitolo è illustrata l’organizzazione in packages, l’esecuzione di un programma
Go e la gestione dell’errore.
2.1 Organizzazione in package e dipendenze
La proprietà di modularità di un programma Go è garantita dall’organizzazione in
packages. Questi sono chiaramente un riferimento al linguaggio Java. Infatti, come prima
riga di un file .go è dichiarato il package di appartenenza, seguito poi dai vari import dei
package necessari per il programma in questione. Una proprietà importante è che
nell’import può essere inserita una URL, quindi c’è la possibilità di avere dei packages
remoti. Tramite il comando go get si ottiene il package dall’URL e lo si installa, se ne fa
l’import e lo si usa come un qualsiasi package. Il comando go get scarica le dipendenze
ricorsivamente, il naming dei package è decentralizzato e scalabile, essendo delegato alle
URL l’allocazione dello spazio dei path degli import. [1]
Il problema delle dipendenze è stato affrontato con particolare attenzione dagli ingegneri di
Mountain View. Nel particolare è stato messo in discussione tutto il sistema degli #include
e degli #ifndef contenuti negli header files del C/C++, in quanto sofferenti di un grave
problema di scalabilità. Tutte queste istruzioni prevedono, infatti, che il precompilatore
controlli molteplici volte gli stessi files, con conseguente incremento delle operazioni di
I/O. Seguendo lo standard ANSI, devono essere dichiarate le librerie utilizzate all’inizio di
ogni file sorgente, con il conseguente inserimento multiplo delle stesse librerie oltre alla
naturale dipendenza concatenata fra queste, cosa che richiede un tempo di compilazione
molto elevato. I tecnici Google hanno stimato che, a fronte un file binario di circa 4MB,
16
espandendo tutti #include, al compilatore giungano circa 8GB di files da analizzare. In
aggiunta a ciò, per i files binari di Mountain View sono necessari grandi sistemi di
compilazione distribuiti, e un tempo medio di 30-45 minuti. [1] Le dipendenze nel
linguaggio Go sono sintatticamente e semanticamente definite dal linguaggio, vengono
dichiarate con la parola chiave import seguita da una stringa costante. Per garantire la
scalabilità ed evitare di compilare codice non necessario, le dipendenze non usate sono
segnalate come errore di compilazione. L’efficienza è invece garantita dal compilatore
poiché all’atto di un import legge il file oggetto della libreria e non il codice sorgente.
Supponendo di avere un programma A che importa B che a sua volta importa C, il
compilatore compila prima C, poi compila B generando il file oggetto, e quando si trova
all’import di B nel programma A semplicemente legge il file oggetto di B, invece di
leggere il file sorgente e ritornare a compilare C. Con questo meccanismo, non originale
ma presente anche in linguaggi come Java, la compilazione risulta quaranta volte più
veloce del linguaggio C, secondo il parere di Rob Pike. [1]
2.2 Compilazione ed esecuzione
Il processo di compilazione è del tutto analogo al C/C++, i file sorgenti .go vengono
compilati generando i file oggetto .o, per poi essere passati al linker che li assembla. [6]
Mediante il comando go build viene lanciato il compilatore, il quale dalla versione 1.5 è
scritto interamente in Go.
Per quanto concerne l’esecuzione di un programma Go, tramite la variabile d’ambiente
GOMAXPROCS si stabilisce il limite dei threads simultanei del sistema operativo che
possono eseguire il codice Go. [7] L’esecuzione di un programma comincia con la
funzione main, la quale non ha parametri di ingresso e di ritorno, ed è necessariamente
dichiarata all’interno del package main. [4]
Anche Go prevede la possibilità di passare argomenti a riga di comando alla funzione
main. Si utilizza la slice Args del package os, ponendo attenzione al fatto che ad os.Args[0]
corrisponde il nome del programma, e solo dall’indice 1 a seguire ci sono gli argomenti
passati. [8]
17
2.3 Errori, panic e recover
La gestione degli errori, inizialmente del tutto assente, è stata introdotta con un approccio
più leggero, e sacrifica la brevità del codice a vantaggio di una maggiore leggibilità.
Questo tipo di gestione viene sconsigliata dagli autori, i quali premono sull’utilizzo dei
valori multipli di ritorno delle funzioni per segnalare gli errori. Il motivo di questo
atteggiamento è la tendenza dei programmatori ad ignorare gli eventi eccezionali, come in
Java, invece che gestirli appropriatamente. [1] Con la versione 1.0, il linguaggio si
arricchisce dell’interfaccia error così dichiarata: [4]
type error interface {
Error() string
}
Per gli eventi eccezionali, il linguaggio mette a disposizione le funzioni panic e recover,
una sorta di equivalenti al costrutto try-catch del C++, che hanno la seguente firma: [4]
func panic(interface{})
func recover() interface{}
La chiamata alla funzione panic fa terminare immediatamente la funzione che la contiene,
ma vengono eseguite normalmente le funzioni differite dichiarate. Al termine delle
funzioni deferred, il controllo viene restituito al chiamante della funzione contenente il
panic, e così via, cioè ripercorrendo a ritroso la catena dei chiamanti fino a giungere alla
funzione main, la quale provoca la terminazione del programma (stack unwinding). Questa
catena di rilancio dello stato di panico può essere interrotta solo dalla funzione recover,
che ha senso ovviamente solo all’interno di una funzione differita. La funzione recover,
che ha quindi il compito di recuperare lo stato di un programma per evitarne la
terminazione anticipata e riprenderne il controllo, deve gestire la specifica situazione di
errore. Recover restituisce valore nil se l’argomento del panic è nil, cioè se la routine non è
in stato di panico e se essa stessa non è inserita in una funzione differita.
18
Capitolo 3: Aspetti avanzati
In questo capitolo saranno illustrati gli aspetti avanzati del linguaggio Go.
3.1 Modello di allocazione delle variabili e garbage collector
Il compilatore Go adotta diverse strategie riguardo all’allocazione delle variabili. Nel caso
di variabili locali a una funzione, il compilatore le alloca all’interno dello stack frame della
funzione stessa. Invece, vengono allocate nella zona heap del garbage collector se il
compilatore non può verificare l’esistenza di riferimenti dopo la fine della funzione.
Vengono allocate, poi, nella zona heap se sono di grandi dimensioni. [9]
Se con la gestione degli errori gli ingegneri di Google hanno avuto delle remore, riguardo
al garbage collector non c’è stato alcun dubbio. [1] Il linguaggio Go è dotato quindi di un
garbage collector, che è l’unico modo per liberare la memoria allocata. Nei linguaggi ove
questo non è previsto, come C/C++, i programmatori spendono molte righe di codice, che
andrebbero tenute nascoste, per allocare e liberare la memoria, esponendoli ad errori
comuni. Essendo un linguaggio concorrente, il garbage collector è fondamentale per
evitare comportamenti malevoli fra le esecuzioni concorrenti. Il garbage collector porta
con sé un aumento dell’overhead di sistema, latenza, oltre che una complessità di
implementazione. L’implementazione alla versione corrente, 1.9, è di tipo mark-and-sweep
parallelo. [10]
3.2 Polimorfismo
Il polimorfismo, cioè la capacità di tipi diversi di rispondere in maniera diversa a uno
stesso messaggio, in Go è realizzato grazie al concetto di interfaccia. Riprendendo il
19
classico esempio del calcolo dell’area per un rettangolo e un cerchio, si può definire
l’interfaccia Shape che contiene il metodo Area() e le struct Rettangolo e Cerchio che
implementano il metodo della Shape. A questo punto basta dichiarare una slice di Shape,
contenente vari rettangoli e cerchi, e chiamare su ogni elemento il metodo Area: così si
assiste al comportamento polimorfico a tempo di esecuzione.
3.3 Gestione dell’I/O
La gestione dell’I/O di Go è, in prima analisi affidata al package fmt. [11] Per la lettura
dell’input da tastiera esistono le seguenti funzioni:
func Scan(a …interface{}) (n int, err error)
func Scanf(format string, a …interface{}) (n int, err error)
func Scanln(a …interface{}) (n int, err error)
che restituiscono il numero di byte letti ed eventuali errori. Nel particolare le funzioni Scan
e Scanln memorizzano in argomenti successivi gli elementi separati dallo spazio, ma la
Scanln si arresta all’arrivo di newline. Queste tre funzioni sono anche disponibili con il
prefisso “F”, cioè Fscan, Fscanf e Fscanln, che hanno bisogno di uno specifico io.Reader.
Per le operazioni di output a video, invece, sono a disposizione le seguenti funzioni:
func Printf(format string, a …interface{}) (n int, err error)
func Println(a …interface{}) (n int, err error)
di cui la prima stampa e va a capo, la seconda segue una formattazione. In realtà, queste
operazioni di scrittura e lettura, si basano sulle variabili Stdin e Stdout del package os. [8]
L’alternativa a tutte queste funzioni è rappresentata dal package bufio, [12] che
implementa l’I/O bufferizzato, con dei wrapper per io.Reader e io.Writer.
La gestione dell’I/O su file è delegata al package os, [8] tramite le consuete funzioni:
func Open(name string) (*File, error)
func (f *File) Close() error
func (f *File) Read(b []byte) (n int, err error)
func (f *File) Write(b []byte) (n int, err error)
Per una gestione avanzata delle operazioni di I/O su file è possibile utilizzare i package
bufio [12] e ioutil. [13]
20
3.4 Gestione della concorrenza e parallelizzazione
La gestione della concorrenza è uno dei nodi principali del linguaggio di Google. Per
quanto concerne le primitive per la definizione di attività concorrenti, è stato illustrato nel
paragrafo 1.7 l’utilizzo delle goroutines. Per la sincronizzazione e la comunicazione fra le
goroutines, il linguaggio Go prevede, oltre alla classica gestione tramite mutex, l’utilizzo
dei canali.
Questo tipo di approccio è basato su un motto creato in casa Google: “Non comunicare
condividendo la memoria; ma, condividere la memoria comunicando”. [5] Il problema
della comunicazione è stato affrontato dagli ingegneri di Mountain View in base al loro
dominio del problema, principalmente web servers, e si basa sul modello communicating
sequential processes (CSP) di Hoare. [1] Per l’utilizzo dei canali, è necessario distinguere
la casistica di canali bufferizzati o non bufferizzati. Nel caso di un canale bufferizzato,
l’operazione di send avviene prima del completamento della corrispondente receive.
Quindi, un tipo di sincronizzazione fra due goroutines potrebbe essere: la prima modifica
una variabile ed effettua una send sul channel, la seconda usa la stessa variabile dopo la
receive sul canale, così è garantito che la seconda goroutine usi la variabile già modificata
dalla prima. Nel caso, invece, di un canale non bufferizzato è la receive che avviene prima
che la send completi. Riprendendo l’esempio precedente, la prima goroutine modifica una
variabile ed effettua una receive sul canale, e la seconda usa la variabile dopo una send
sullo stesso canale. Più in generale, è possibile modellare un semaforo con un canale
bufferizzato, il numero di elementi del canale rappresenta gli utenti attivi, la capacità del
canale i permessi del semaforo, le operazioni di send e receive rappresentano
rispettivamente l’acquisizione e il rilascio di un permesso sul semaforo. [14]
L’utilizzo del package sync è scoraggiato dai creatori di Go, che premono per l’utilizzo dei
canali. Nel package, comunque, vengono messe a disposizione le primitive per i mutex:
[15]
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
a cui vengono aggiunte le condizioni per il costrutto monitor:
func NewCond(l Locker) *Cond
21
func (c *Cond) Wait()
func (c *Cond) Signal()
Il linguaggio Go, essendo concorrente, non soddisfa tutti i problemi del modello della
parallelizzazione. Un meccanismo per parallelizzare il codice può essere: dividere un
problema in diverse parti, lanciare tante funzioni quante sono i core della CPU, ricavabili
tramite runtime.NumCPU() [7], e sincronizzare il tutto attraverso un channel che attende
che tutti i tasks abbiano terminato. [5]
3.5 Cenni a rete, socket e design patterns
Nel package net, il linguaggio Go mette a disposizione le funzionalità per il supporto alla
rete, fra le quali UDP e TCP. [16] Fra le funzioni di maggior rilevanza ed uso, possono
essere citate:
func Dial(network, address string) (Conn, error)
func Close() error
func Read(b []byte) (n int, err error)
func Write(b []byte) (n int, err error)
ove come parametro network è lecito indicare “tcp” o “udp”, ad esempio.
Lato server, invece, sono disponibili le seguenti:
func Listen(network, address string) (Listener, error)
func (l *Listener) Accept() (Conn, error)
func (l *Listener) Close() error
Nella programmazione Go possono essere usati anche i design patterns, cioè soluzioni
generali a problemi ricorrenti, definiti dalla Gang of Four. Per lo sviluppo dei singoli
pattern, avendo ben presente l’impossibilità di realizzarli se è prevista l’ereditarietà, si
rimanda a GitHub, al progetto go patterns, che espone le implementazioni specifiche per
delega in Go. [17]
22
Capitolo 4: Confronto con C++ e Java
In questo capitolo verranno confrontati Go, C++ 2011, e Java in base alle caratteristiche.
C+
+ 2
011
Java
Go
Paradigma di
programmazione
Pro
cedura
le
Sì
Sì
Sì
Fun
zional
e S
ì S
ì S
ì
Gen
eric
a S
ì S
ì N
o, m
a non è
esc
lusa
in
futu
ro [
9]
Object Orientation
Cost
rutt
o
clas
se
Sì
Sì
No, st
ruct
+ m
etodi
Inca
psu
lam
ento
S
ì, c
on s
pec
ific
atori
di
acce
sso
Sì,
con s
pec
ific
atori
di
acce
sso
Sì,
tra
mit
e up
per
e l
ow
er
case
Ere
dit
arie
tà
Sì
Sì
No, so
lo c
om
posi
zione
Poli
morf
ism
o
Sì
Sì
Sì,
tra
mit
e in
terf
acce
Gestione
della
memoria
Punta
tori
S
ì N
o
Sì,
ma
senza
ari
tmet
ica
Gar
bage
coll
ecto
r N
o
Sì
Sì
Gestione
dell'errore
E
ccez
ioni
+ c
ost
rutt
o
try/c
atch
Ecc
ezio
ni
+ c
ost
rutt
o
try/c
atch
Err
ori
com
e val
ori
di
rito
rno m
ult
ipli
del
le
funzi
oni
+ p
anic
e r
ecover
Concorrenza
Thre
ads
Sì
Sì
Sì,
le
goro
uti
nes
Mute
x
Sì
Sì
Sì,
ma
si p
rivil
egia
no i
canal
i
Monit
or
Sì
Sì
Sì,
ma
si p
rivil
egia
no i
canal
i
23
Capitolo 5: Applicazioni esistenti
La crescita del linguaggio Go è dovuta, oltre alle caratteristiche peculiari e la semplicità,
sicuramente all’influenza del colosso Google. L’uso del linguaggio nell’azienda di
Mountain View è riservato, ma per alcuni scopi è di dominio pubblico. L’intero web server
del sito ufficiale di Go è scritto nel linguaggio Go; inoltre, nello sviluppo di Google
Fuchsia, cioè un nuovo sistema operativo che dovrebbe essere capace di funzionare su ogni
tipo di dispositivo, è stato adoperato il linguaggio Go in sinergia a C, C++, Rust, Dart,
Python e Swift. Nel panorama mondiale, il linguaggio si sta diffondendo non solo in
aziende che si occupano esplicitamente di software e hardware. Fra le aziende di tipo
informatico sono da citare come esempio: Adobe, Dell, General Electric Software, IBM,
Intel, Docker, DropBox MongoDB, Mozilla, Oracle e VMware, le quali più o meno
esplicitamente hanno dichiarato l’utilizzo del linguaggio di Google. Accanto a queste sono
da menzionare i social network come Facebook, Pinterest, Reddit, Tumblr e Twitter,
nonché nel settore dell’informazione la BBC, The Economist, The New York Times. Il
linguaggio Go ha anche spaziato nel settore entertainment con Mattel, Netflix, Riot Games
e Carbon Games, oltre che nell’e-commerce con Ebay e Zalando. L’impiego di Go è
giunto, inoltre, nella sfera della medicina e dei trasporti con Novartis, Ryanair e la
contestatissima Uber. Queste sono solo alcune delle aziende di portata internazionale che
utilizzano il linguaggio Go, ma esistono anche altre migliaia di piccole aziende locali che
ne fanno impiego.
24
Capitolo 6: Sviluppo di un programma in Go
Nel seguente capitolo viene sviluppato un programma Go a titolo esemplificativo, per
esporre in maniera ancora più chiara le funzionalità del linguaggio.
6.1 Installazione e ambienti di sviluppo
Nella sezione download del sito ufficiale del linguaggio Go è possibile scaricare i file
binari dell’ultima versione stabile, 1.9.3, per Microsoft Windows, Apple macOS e Linux.
Analogamente, dal sito ufficiale, è possibile scaricare i plugin che sono ora disponibili per
molteplici IDE, quali Visual Studio Code, vim, Atom. Per lo sviluppo del programma del
successivo paragrafo è stato adoperato il plugin GoClipse per Eclipse in ambiente
Windows.
6.2 Esempio di programma in Go
La scelta di un esempio da sviluppare è stata presa considerando la volontà di voler coprire
più caratteristiche possibili trattate del linguaggio Go in chiave puramente didattica. Il
programma è basato sul design pattern Proxy-Skeleton, quindi con una parte client e una
parte server che comunicano attraverso le socket, con l’aggiunta di scrittura su file e
goroutines e channels. In linguaggio pseudo-UML può essere così visto il class diagram
lato server:
25
Dal lato client, invece, sempre in pseudo-UML:
ove gli Stub e lo Skeleton si occupano della comunicazione su rete.
Nel seguito verranno presentate le righe di codice salienti dei file sorgenti necessari per
questo programma.
L’interfaccia è comune a entrambe le parti, ed è la seguente
type InterfacciaServizi interface {
Invia(num int)
Ricevi() int
}
Iniziando dal lato client nel file client.go è presente la funzione main
func main() {
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go clientRoutine(i, done)
}
for i := 0; i < 10; i++ {
<-done
}
fmt.Println("Finito!")
}
che genera 10 goroutines e ne attende la terminazione tramite il canale done. La funzione
clientRoutine
func clientRoutine(n int, done chan bool) {
var st Stub
if n%2 == 0 {
val := st.Ricevi()
fmt.Printf("Sono la routine %d e ho ricevuto %d\n", n, val)
} else {
val := rand.Intn(100)
st.Invia(val)
fmt.Printf("Sono la routine %d e ho inviato %d\n", n, val)
}
done <- true
}
se n è pari si comporta da consumatore chiamando la funzione Ricevi dello Stub, altrimenti
genera un numero casuale e chiama la funzione Invia. In entrambi i casi notifica la
terminazione al canale done.
26
Lo Stub, che implementa le funzioni dell’interfaccia, rende la comunicazione su rete
trasparente al client con l’istanziazione di una socket tcp sul porto scelto casualmente 5423
s.conn, _ = net.Dial("tcp", "127.0.0.1:5423")
defer s.conn.Close()
con l’utilizzo della parola chiave defer per assicurarsi di chiudere la connessione al termine
della funzione chiamante. Interessante, sempre in stub.go, è questo frammento di codice
func (s *Stub) Ricevi() int {
//omissis
val, err := strconv.Atoi(message)
defer postpanico()
if err != nil {
panic(err)
}
return val
}
func postpanico() {
if r := recover(); r != nil {
fmt.Println("Provo a recuperare lo stato")
}
che mostra il comportamento di panic e recover a tempo di esecuzione. Nel caso in cui la
funzione Atoi generi un errore poiché impossibilitata a convertire la stringa message in
intero, la variabile err assume valore diverso da nil e viene lanciato il panico. A tempo di
esecuzione non viene stampato lo stack trace dell’errore, ma viene eseguita la funzione
postpanico in maniera differita, la quale contiene la funzione recover che restituisce un
valore diverso da nil solo se si è verificata una chiamata a panic.
Lato server, invece, è presente il file server.go che contiene semplicemente la funzione
main
func main() {
var s Skeleton
s.RunSkeleton()
}
che istanzia uno Skeleton e ne chiama la funzione RunSkeleton.
Lo Skeleton, al pari dello Stub, si occupa della comunicazione su rete e implementa
InterfacciaServizi. Possiede una variabile privata di serverReale, inoltre si pone in attesa di
connessioni tcp sul medesimo porto 5423 e chiama una differente goroutine ad ogni
richiesta di connessione.
27
func (s *Skeleton) RunSkeleton() {
//omissis
ln, _ := net.Listen("tcp", "127.0.0.1:5423")
for {
conn, _ := ln.Accept()
go skeletonroutines(s, conn)
}
}
Le goroutines elaborano i messaggi di richiesta chiamando le funzioni Invia e Ricevi
implementate dallo Skeleton e nel secondo caso si occupano anche di costruire ed inviare il
messaggio di risposta. L’implementazione delle funzioni dell’interfaccia prevede solo la
delega alle rispettive funzioni del serverReale.
func (s *Skeleton) Invia(num int) {
s.ser.Invia(num)
}
Nel file serverReale.go è definito il seguente tipo
type serverReale struct {
valore int
prod chan bool
cons chan bool
miofile *os.File
}
che ha come attributi un buffer unitario di tipo int, un puntatore a un file e due canali
bidirezionali utili per accedere in mutua esclusione al buffer. La funzione Inizializza
func (s *serverReale) Inizializza() {
s.valore = 0
s.miofile, _ = os.Create("miofile.txt")
s.prod = make(chan bool, 1)
s.cons = make(chan bool, 1)
s.prod <- true
}
crea un file chiamato miofile.txt, istanzia i due canali bufferizzati di capacità 1 ed invia un
valore true sul canale prod poiché il buffer è inizialmente vuoto e pronto alla produzione.
La funzione Invia si occupa della produzione del valore:
func (s *serverReale) Invia(num int) {
<-s.prod
s1 := "Produco: " + strconv.Itoa(num)
fmt.Println(s1)
s.valore = num
s.miofile.WriteString(s1 + "\r\n")
time.Sleep(2 * time.Second)
s.cons <- true
}
28
Nel particolare aspetta la ricezione dal canale prod, che indica che il buffer è vuoto,
effettua la produzione, scrive su file e si pone in attesa di 2 secondi. Al termine segnala
tramite invio di true sul canale cons che un valore è stato scritto ed è pronto per essere
consumato.
La funzione Ricevi, invece, si occupa del consumo dal buffer:
func (s *serverReale) Ricevi() int {
<-s.cons
s1 := "Consumo: " + strconv.Itoa(s.valore)
fmt.Println(s1)
val := s.valore
s.miofile.WriteString(s1 + "\r\n")
s.prod <- true
return val
}
La funzione si pone in attesa tramite la ricezione sul canale cons, consuma il valore, scrive
sul file e al termine invia true al canale prod per segnalare che il buffer è vuoto. Per
realizzare la mutua esclusione è stato scelto l’utilizzo dei canali per seguire lo stile di
programmazione Go, così come fortemente spinto dai creatori, a discapito dei mutex del
package sync.
Il codice sorgente integrale è presente in appendice a questo elaborato.
29
Conclusioni
Le critiche mosse al linguaggio Go corrispondono proprio alle sue caratteristiche, come per
esempio, la mancanza dei tipi generici, l’assenza dell’overloading che lo rende più
prolisso, l’assenza dell’ereditarietà e la presenza del garbage collector che genera pause ed
overhead del sistema.
Gli sviluppatori del linguaggio hanno dichiarato che nessun nuovo linguaggio è nato e si è
affermato nel nuovo millennio. Neppure il quasi decennale Go, però, è riuscito a
pareggiare linguaggi come Java e C++. Google, probabilmente, in un primo momento l’ha
trattato come esperimento, dedicando poco tempo e risorse. Invece avrebbe potuto
investire molto e imporlo, grazie alla propria influenza, a livello mondiale come il
corrispondente Swift per Apple.
Con lo standard di C++ del 2011 poi, il Go, nonostante sia nettamente antecedente, ha
perso il proprio punto di forza, la gestione della concorrenza.
Il linguaggio Go è sicuramente stimolante e genera molta curiosità al riguardo. Ha delle
caratteristiche molto interessanti, frutto di una volontà di fare davvero innovazione da parte
degli sviluppatori. Queste caratteristiche spingono i programmatori a ripensare a come
sviluppare il proprio codice per allinearsi allo stile di Go. Se da un lato questo implica
impegno, poiché non immediato da assimilare, dall’altro Go ha dalla propria parte la
semplicità della sintassi e l’estrema leggibilità del codice.
La scelta di studiare ed usare il linguaggio Go potrà essere utile per il futuro, se Google
deciderà di investire più tempo e risorse, ma viste le centinaia di linguaggi esistenti,
l’impegno da parte di BigG dovrà essere sicuramente maggiore affinché sia davvero
conveniente migrare a questo nuovo linguaggio.
30
Bibliografia
[1] Go at Google, https://talks.golang.org/2012/splash.article, 11 gennaio 2018
[2] TIOBE Index – The software quality company, https://www.tiobe.com/tiobe-index/,
12 gennaio 2018
[3] The Go Programming Language – Release History,
https://golang.org/doc/devel/release.html, 11 gennaio 2018
[4] The Go Programming Language Specification, https://golang.org/ref/spec, 13
gennaio 2018
[5] The Go Programming Language – Effective Go,
https://golang.org/doc/effective_go.html, 13 gennaio 2018
[6] The Go Programming Language – Command compile,
https://golang.org/cmd/compile/, 18 gennaio 2018
[7] The Go Programming Language – Package Runtime, https://golang.org/pkg/runtime/,
18 gennaio 2018
[8] The Go Programming Language – Package os, https://golang.org/pkg/os/, 18 gennaio
2018
[9] The Go Programming Language – Frequently Asked Questions (FAQ),
https://golang.org/doc/faq, 27 gennaio 2018
[10] Go 1.4+ GC Plan and Roadmap,
https://docs.google.com/document/d/16Y4IsnNRCN43Mx0NZc5YXZLovrHvvLhK_h0K
N8woTO4/edit, 21 gennaio 2018
[11] The Go Programming Language – Package fmt, https://golang.org/pkg/fmt/, 24
gennaio 2018
31
[12] The Go Programming Language – Package bufio, https://golang.org/pkg/bufio/, 24
gennaio 2018
[13] The Go Programming Language – Package ioutil, https://golang.org/pkg/io/ioutil/, 24
gennaio 2018
[14] The Go Programming Language – The Go Memory Model,
https://golang.org/ref/mem, 27 gennaio 2018
[15] The Go Programming Language – Package sync, https://golang.org/pkg/sync/, 27
gennaio 2018
[16] The Go Programming Language – Package net, https://golang.org/pkg/net/, 29
gennaio 2018
[17] GitHub tmrts/go-patterns, https://github.com/tmrts/go-patterns, 29 gennaio 2018
32
Appendice
In questa appendice è riportato integralmente il codice sorgente sviluppato come esempio
nel paragrafo 6.2.
File client.go
package main
import (
"fmt"
"math/rand"
)
func main() {
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go clientRoutine(i, done)
}
for i := 0; i < 10; i++ {
<-done
}
fmt.Println("Finito!")
}
func clientRoutine(n int, done chan bool) {
var st Stub
if n%2 == 0 {
val := st.Ricevi()
fmt.Printf("Sono la routine %d e ho ricevuto %d\n", n, val)
} else {
val := rand.Intn(100)
st.Invia(val)
fmt.Printf("Sono la routine %d e ho inviato %d\n", n, val)
}
done <- true
}
33
File stub.go
package main
import (
"fmt"
"net"
"strconv"
"strings"
)
type InterfacciaServizi interface {
Invia(num int)
Ricevi() int
}
type Stub struct {
conn net.Conn
}
func (s *Stub) Invia(num int) {
s.conn, _ = net.Dial("tcp", "127.0.0.1:5423")
defer s.conn.Close()
s1 := "PUT_" + strconv.Itoa(num)
s.conn.Write([]byte(s1 + "\n"))
}
func (s *Stub) Ricevi() int {
s.conn, _ = net.Dial("tcp", "127.0.0.1:5423")
defer s.conn.Close()
s1 := "GET"
s.conn.Write([]byte(s1 + "\n"))
b := make([]byte, 20)
n, _ := s.conn.Read(b)
message := string(b[:n])
message = strings.TrimSuffix(message, "\n")
val, err := strconv.Atoi(message)
defer postpanico()
if err != nil {
panic(err)
}
return val
}
func postpanico() {
if r := recover(); r != nil {
fmt.Println("Provo a recuperare lo stato")
}
}
File server.go
package main
func main() {
var s Skeleton
s.RunSkeleton()
}
34
File skeleton.go
package main
import (
"net"
"strconv"
"strings"
)
type InterfacciaServizi interface {
Invia(num int)
Ricevi() int
}
type Skeleton struct {
ser serverReale
}
func (s *Skeleton) RunSkeleton() {
s.ser.Inizializza()
ln, _ := net.Listen("tcp", "127.0.0.1:5423")
for {
conn, _ := ln.Accept()
go skeletonroutines(s, conn)
}
}
func (s *Skeleton) Invia(num int) {
s.ser.Invia(num)
}
func (s *Skeleton) Ricevi() int {
return s.ser.Ricevi()
}
func skeletonroutines(s *Skeleton, conn net.Conn) {
b := make([]byte, 20)
n, _ := conn.Read(b)
message := string(b[:n])
message = strings.TrimSuffix(message, "\n")
if message == "GET" {
val := s.Ricevi()
risposta := strconv.Itoa(val)
conn.Write([]byte(risposta + "\n"))
} else if strings.Contains(message, "PUT_") {
s1 := strings.Replace(message, "PUT_", "", 1)
num, _ := strconv.Atoi(s1)
s.Invia(num)
}
}
35
File serverReale.go
package main
import (
"fmt"
"os"
"strconv"
"time"
)
type serverReale struct {
valore int
prod chan bool
cons chan bool
miofile *os.File
}
func (s *serverReale) Inizializza() {
s.valore = 0
s.miofile, _ = os.Create("miofile.txt")
s.prod = make(chan bool, 1)
s.cons = make(chan bool, 1)
s.prod <- true
}
func (s *serverReale) Invia(num int) {
<-s.prod
s1 := "Produco: " + strconv.Itoa(num)
fmt.Println(s1)
s.valore = num
s.miofile.WriteString(s1 + "\r\n")
time.Sleep(2 * time.Second)
s.cons <- true
}
func (s *serverReale) Ricevi() int {
<-s.cons
s1 := "Consumo: " + strconv.Itoa(s.valore)
fmt.Println(s1)
val := s.valore
s.miofile.WriteString(s1 + "\r\n")
s.prod <- true
return val
}