Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Dipartimento di Informatica
Relatore: Candidato:
Prof. Gian Luca Pozzato Yassine Amri
1
DICHIARAZIONE DI ORIGINALITÀ
“Dichiaro di essere responsabile del contenuto dell’elaborato che presento al
fine del conseguimento del titolo, di non avere plagiato in tutto o in parte il la-
voro prodotto da altri e di aver citato le fonti originali in modo congruente alle
normative vigenti in materia di plagio e di diritto d’autore. Sono inoltre con-
sapevole che nel caso la mia dichiarazione risultasse mendace, potrei incorrere
nelle sanzioni previste dalla legge e la mia ammissione alla prova finale potrebbe
essere negata.”
2
Alla mia famiglia
Contents
1 Introduzione 5
1.1 Go . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Chat e Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.3 Obiettivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2 Linguaggio Go 6
2.1 Dichiarazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.1.1 Variabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.1.2 Puntatori . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.3 Strutture . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2 Funzioni e Metodi . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.1 Funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.2.2 Metodi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3 Interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.4 Goroutine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.5 Channel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.6 Controllo del flusso . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.6.1 Defer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.6.2 Panic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.6.3 Recover . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3 Caratteristiche principali di Go 16
3.1 Compilazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.1.1 Compilazione . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.1.2 Compilazione incrociata . . . . . . . . . . . . . . . . . . . 17
3.2 Concorrenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.2.1 Concorrenza e Parallelismo . . . . . . . . . . . . . . . . . 18
3.2.2 Goroutine vs Thread . . . . . . . . . . . . . . . . . . . . . 19
3.2.3 Sincronizzazione e Comunicazione . . . . . . . . . . . . . 20
3.2.4 Prestazioni . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.3 OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.3.1 Incapsulamento . . . . . . . . . . . . . . . . . . . . . . . . 26
3.3.2 Ereditarietà e Composizione . . . . . . . . . . . . . . . . . 27
3.3.3 Polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.3.4 Astrazione . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.4 Garbage Collection . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.5 Dipendenze . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.6 Meno è esponenzialmente meglio . . . . . . . . . . . . . . . . . . 33
4 Conclusioni 34
4
1 Introduzione
1.1 Go
Go è un linguaggio di programmazione open source sviluppato da Google e
supportato da un grande numero di contributori.
Viene anche chiamato Golang ed è un linguaggio ispirato a C, tipizzato
in modo statico e dotato di una compilazione veloce. È stato sviluppato per
velocizzare e semplificare la scrittura di programmi altamente concorrenti e
scalabili. Difatti viene usato per lo sviluppo di applicazioni server, applicazioni
distribuiti, middleware e database.
Go viene concepito nel 2007 da alcuni sviluppatori Google: Robert Griese-
mer, Rob Pike e Ken Thompson. Solo nel 2012 viene rilasciata ufficialmente
con la versione 1.0. Ad oggi, maggio 2022, siamo alla versione 1.18. Le funzion-
alità iniziali sono state progressivamente migliorate e ne sono state introdotte
delle nuove come recentemente è accaduto con i tipi generici. Queste modi-
fiche sono rese possibili da Google, in grado di garantire supporto e longevità
al linguaggio, e dalla community di Gophers che risulta essere una delle com-
munity più attive e propense a trovare problematiche e condividere soluzioni e
implementazioni.
1.3 Obiettivi
Nel capitolo 2 viene introdotta una breve documentazione in grado di dare le
nozioni principali di Go, in modo da permettere la comprensione degli argomenti
successivi.
Nel capitolo 3 vengono mostrate le caratteristiche principali di Go, ma con
un’ottica di paragone sopratutto riguardo allo stile di programmazione, alla
performance di compilazione e al modello concorrenziale che è in grado di offrire.
5
2 Linguaggio Go
2.1 Dichiarazioni
2.1.1 Variabili
La variabile è una porzione di memoria adibita alla memorizzazione di un valore
di un tipo accessibile tramite un identificatore o nome. Esistono diversi modi
per dichiarare una variabile. La più comune è la dichiarazione di una singola
variabile:
var numberRooms = 0
numberMessages := 0
var (
numberUsers int
numberMessages = 0
)
(1)(2)
6
2.1.2 Puntatori
Un puntatore è una variabile in grado di memorizzare la locazione degli ele-
menti in memoria. Per dichiarare un puntatore basta semplicemente utilizzare
l’operatore * prima del tipo. Così facendo potrà puntare solo a porzioni di
memoria che contengono valori dello stesso tipo.
b := 5
var a *int = &b
Il valore zero di un puntatore è il valore nullo, ovvero nil. Invece per dereferen-
ziare un puntatore, cioè accedere al valore della variabile puntata, è necessario
mettere l’operatore * prima del nome del puntatore. In sua mancanza si ottiene
l’indirizzo di memoria del valore puntato:
fmt.Println("l’indirizzo di b: ", a)
fmt.Println("il valore di b: ", *a)
Quello che invece non si può fare è usare l’aritmetica dei puntatori in quanto
Go non la supporta. (1)(2)
2.1.3 Strutture
Una struct è un costrutto che ci permette di definire un nuovo tipo di dato e che
aggrega al suo interno una raccolta di variabili. Può essere comunemente utiliz-
zato in momenti in cui ha senso raggruppare i dati in una singola unità anziché
avere ciascuno di essi come valori separati, come ad esempio la creazione di una
entità logica (nome della struttura) avente determinate caratteristiche (variabili
di cui è composta). In questo caso viene creata una struttura PublicMessage
che rappresenta l’entità “messaggio pubblico”:
7
il campo a cui si vuole accedere.
Il valore zero di una struttura, ovvero quando non avviene assegnato alcun
valore ai campi che la compongono, è rappresentato dai valori zero di ciascun
campo in base al proprio tipo di riferimento. (1)(2)
Questo codice di esempio rappresenta una funzione di nome NewUser che prende
come parametro la stringa username e ritorna un puntatore a un tipo User.
Una peculiarità interessante di Go è la possibilità di ritornare più di ele-
mento.
if err != nil {
return nil, "cannot handle this packet"
}
8
tatore a un tipo Response e una stringa vuota. In caso di errore invece ritorna
nil e la stringa “cannot handel this packet”.
Ma l’utilizzo più importante di più valori di ritorno e quello di gestire gli
errori. Si prenda come esempio la stessa funzione Handle(), ma con una piccola
modifica:
if err != nil {
return nil, err
}
Come si può notare tra i tipi di ritorno è presente il tipo error, che è una
interfaccia che gestisce i vari tipi di errori gestiti da Go. Ovviamente questi
errori sono personalizzabili dal programmatore qualora lo ritenesse necessario.
Dunque se si è in presenza di un errore viene stampata una stringa che ne indica
la natura, altrimenti viene posto a nil. (1)(2)
2.2.2 Metodi
Un metodo è semplicemente una funzione in cui viene indicato un ricevitore
tra la keyword func e il nome del metodo. Il ricevitore è, solitamente, di tipo
struct, ma può essere di qualsiasi tipo e serve per far si che solo quel particolare
ricevitore possa richiamare quel metodo. L’esempio successivo gestisce la dis-
connessione di un client dal server e come ricevitore vine indicato un puntatore
a una struttura di tipo client.
// client.go
type Client struct {
user *entity.User
connection *net.Conn
chain *chain.Chain
removeUser *usecase.RemoveUserUseCase
}
9
)
client.user = nil
}
client.disconnectUser();
2.3 Interfacce
In Go, un’interfaccia è un costrutto simile a una struttura, in grado di contenere
un insieme di metodi. Si dice che un tipo implementa un’interfaccia quando è
un ricevitore di tutti i metodi definiti all’interno di essa. Dunque l’interfaccia
specifica quali metodi dovrebbe avere un tipo e quest’ultimo decide come im-
plementarli. Si prenda in considerazione un esempio che racchiuda le principali
caratteristiche.
10
func (p *PublicMessage) SentAt() time.Time {
return p.sentAt
}
2.4 Goroutine
In Go ogni attività di esecuzione concorrente viene chiamata Goroutine. Una
goroutine non è altro che un metodo o una funzione in grado di essere eseguito
in parallelo con tutte le altre goroutine, ovvero di avere un flusso di esecuzione
separato. Erroneamente vengono confuse con i thread, i quai presentano delle
caratteristiche diverse e spesso limitanti rispetto alle goroutine.
All’avvio di un programma esiste una sola goroutine ed è quella che chiama la
funzione main, che viene comunemente chiamata goroutine principale. Quindi
ogni goroutine creata successivamente verrà eseguita parallelamente a quella
principale. Per quanto riguarda, invece, la creazione di una nuova goroutine è
necessaria una semplice keyword che è omonima del linguaggio stesso, ovvero
go. Questa deve essere anteposta alle funzioni o ai metodi scelti per essere
eseguiti contemporaneamente con le altre esecuzioni concorrenti.
Prendiamo in considerazione una funzione che permette a un qualsiasi client
connesso di gestire i pacchetti provenienti da una connessione TCP:
// client.go
func (client *Client) ListenForPackets() {
defer client.closeClient()
decoder := json.NewDecoder(*client.connection)
for {
var p packet.Packet
if err := decoder.Decode(&p); err == io.EOF {
break
} else if err != nil {
11
panic(err)
}
response, err := client.chain.Handle(p, client.user)
if err != nil {
panic(err)
}
switch response.(type) {
case *handler.LoginResponse:
user := response.(*handler.LoginResponse).GetUser()
if user != nil {
client.connectUser(user)
}
}
for _, p = range response.Packets() {
client.SendPacket(p)
}
}
}
Nel file server.go è, invece, presente il codice che, dopo l’avvenimento di una
connessione, crea un oggetto client. Questo, successivamente, farà la chiamata
al metodo ListenForPackets().
//server.go
for {
connection, _ := listener.Accept() // Gestisce la connessione dei
client
client := client.NewClient(&connection, chain, removeUser) // creo
un oggetto client
server.clients = append(server.clients, client)
go client.ListenForPackets() // creo una goroutine
}
2.5 Channel
Se le goroutine sono le attività che permettono la programmazione concorrente,
i channel sono una sorta di connessione che permette la comunicazione tra
le prime. Un channel è un meccanismo di comunicazione che consente a una
goroutine di inviare valori a un’altra goroutine. Ogni channel è un “condotto”
in grado di contenere i valori di un solo tipo, scelto durante la dichiarazione del
channel.
Inoltre il channel è bidirezionale. Questo permette alle goroutine di inviare
e ricevere dati attraverso lo stesso canale. Vediamo adesso come crearne una
12
contenente un valore di tipo bool:
Come si può vedere è necessaria la funzione make che prende come parametro
chan bool : la prima serve per indicare che si tratta di un channel, mentre la
seconda rappresenta il tipo del valore.
Un channel è una reference a una struttura dati creata da make. Quindi
quanto copiamo un channel o ne passiamo una a una funzione stiamo passando
una reference.
Come detto prima, un channel ha due principali operazioni, scrittura e let-
tura, che prendono il nome di comunicazione. Per poter far ciò abbiamo bisogno
dell’operatore <-, usato sia in fase di lettura che quella di scrittura:
13
Questo codice permette il salvataggio, in modo sincronizzato, di un’entità user
all’interno di un repository. Come si può intuire, la prima e la seconda istruzione
permettono l’acquisizione e il rilascio del lock per accedere alla sezione critica,
che in questo caso è rappresentata dall’ultima istruzione. Nonostante il metodo
Unlock() sia scritto prima, questa verrà eseguita come ultima istruzione della
funzione.
Qualora ci fossero più istruzioni defer, queste verrebbero inserite in uno stack
ed eseguite nell’ordine LIFO (Last In First Out). Quindi l’ultima istruzione defer
è la prima ad essere eseguita e via proseguendo fino al primo defer. (1)(2)
2.6.2 Panic
Nel capito 2.2.1 è stato introdotto il concetto di error e di come questo sia utile
per indicare una sorta di anomalia nel progetto che è in grado di essere gestito
e che, al contrario delle eccezioni, non terminano l’esecuzione del programma.
Difatti gli errori sono sufficienti per la maggior parte delle condizioni anomale
che si verificano nel programma.
Ma ci sono alcune situazioni in cui il programma non può continuare l’esecu-
zione dopo una condizione anomala. In questo caso, usiamo Panic per terminare
prematuramente il programma. Quando una funzione incontra un panic, la
sua esecuzione viene interrotta e tutte le istruzioni defer, qualora ce ne siano,
vengono eseguite. A quel punto il programma stampa il messaggio di panico.
if err != nil {
panic(err)
}
Ci sono due modi per andare in panic: il primo modo avviene quando l’errore
che vogliamo gestire è considerato irreversibile, mentre il secondo avviene per
errori del programmatore come la classica divisione per 0. (1)(2)
2.6.3 Recover
Il Recover è una funzione incorporata che viene utilizzata per riprendere il con-
trollo di un programma che è andato in panic, ovvero per un errore non gestibile
che causa il blocco del flusso di esecuzione.
// client.go
defer client.closeClient()
defer func() {
if r := recover(); r != nil {
client.closeClient()
println("Client disconnect due to panic")
}
}()
decoder := json.NewDecoder(*client.connection)
for {
var p packet.Packet
14
if err := decoder.Decode(&p); err == io.EOF {
break
} else if err != nil {
panic(err)
}
response, err := client.chain.Handle(p, client.user)
if err != nil {
panic(err)
}
switch response.(type) {
case *handler.LoginResponse:
user := response.(*handler.LoginResponse).GetUser()
if user != nil {
client.connectUser(user)
}
}
for _, p = range response.Packets() {
client.SendPacket(p)
}
}
15
3 Caratteristiche principali di Go
3.1 Compilazione
3.1.1 Compilazione
Go è un linguaggio compilato. La caratteristica dei linguaggi compilati come C,
C++ e lo stesso Go è che vengono convertiti direttamente in codice macchina
che il processore è in grado di eseguire.
I linguaggi compilatati richiedono una fase di “costruzione” o di build : devono
quindi essere compilati “manualmente”. Nel caso di Go per generale un file
eseguibile basterà scrivere da terminale
go build main.go
go run main.go
//GO
func main() {
Inputs := [4]int64{10000, 50000, 100000, 500000}
for i := 0; i < len(Inputs); i++ {
var before = time.Now().UnixNano() / int64(time.Millisecond)
var factorial = big.NewInt(1)
var j int64
for j = 1; j <= Inputs[i]; j++ {
factorial = factorial.Mul(factorial, big.NewInt(j))
}
var after = time.Now().UnixNano() / int64(time.Millisecond)
16
var time = after - before
fmt.Println(Inputs[i], time)
}
}
//JAVA
public static void main(String[] args){
int[] allInputs = new int[]{10000, 50000, 100000, 500000};
for(int i=0;i<allInputs.length;i++){
BigInteger number = BigInteger.valueOf(allInputs[i]);
BigInteger result = BigInteger.valueOf(1);
long before = System.currentTimeMillis();
for (int j=1;j<=allInputs[i];j++){
result = result.multiply(BigInteger.valueOf(j));
}
long after = System.currentTimeMillis();
System.out.println("Elapsed Time: "+(after - before));
}
}
17
la distribuzione dei pacchetti, poiché non è necessario avere accesso a una pi-
attaforma specifica per distribuire il pacchetto. I linguaggi interpretati non
hanno bisogno della cross-compilazione in quanto basta che sia installata nella
macchina l’interprete che permetta l’esecuzione del codice. Ma come sappiamo,
oltre a sprecare memoria per l’interprete, i linguaggi interpretati sono più lenti
di quelli compilati. D’altro canto, per quanto riguarda i linguaggi compilati
come C++, non è per niente facile eseguire una cross-compilazione neanche uti-
lizzando CMake. Qualora si stia lavorando in un sistema linux e si volesse poi
passare a un sistema Windows, con C++ il modo più “semplice” sarebbe quella
di installare e configurare WSL (Windows Subsystem for Linux) su Windows
ed eseguire la compilazione. Con Go, invece, la possibilità di cambiare da un
OS ad un altro avviene in modo immediato tramite due variabili d’ambiente:
GOOS e GOARCH. il primo serve per indicare il sistema operativo, mentre il
secondo serve per indicare l’architettura. Inoltre nella fase di build, possiamo
indicare più sistemi operativi e architetture:
3.2 Concorrenza
3.2.1 Concorrenza e Parallelismo
Prima di parlare di come funzionano gli aspetti più caratteristici di Go, ovvero
la gestione della concorrenza e del parallelismo, diamo alcune definizioni:
Concorrenza: è una tecnica utilizzata per diminuire il tempo di risposta del
sistema utilizzando un’unica unità di elaborazione o elaborazione sequenziale.
Un’attività è divisa in più parti e la sua parte viene elaborata contemporanea-
mente ma non nello stesso istante. Produce l’illusione del parallelismo attraverso
la commutazione di contesto, ma in realtà i pezzi di un’attività non vengono
elaborati parallelamente.
Parallelismo: è concepito allo scopo di aumentare il velocità di calcolo uti-
lizzando più processori. È una tecnica per eseguire simultaneamente i diversi
compiti nello stesso istante. Coinvolge diverse unità di elaborazione o dispositivi
di elaborazione indipendenti che operano ed eseguono parallelamente attività al
fine di aumentare la velocità di calcolo e migliorare la produttività.(8)
A differenza di altri linguaggi di programmazione come Java, C++ e Python,
18
Go è stato creato in un contesto storico in cui la gestione della concorrenza era
una problematica all’ordine del giorno e soprattutto erano già diffusi i processori
multi-core. Questo ha reso possibile quindi la creazione di un linguaggio di
programmazione in grado di poter affrontare la programmazione concorrenziale
nel modo più efficace, semplice e soprattutto internamente senza quindi l’ausilio
di implementazioni successive e librerie esterne.
Inoltre abbiamo affrontato il concetto di parallelismo in quanto Go, a dif-
ferenza di altri linguaggi, rende possibile il controllo del numero di thread del
sistema operativo che in quel momento stanno gestendo le varie goroutine. Pos-
siamo fare ciò grazie all’ausilio della variabile d’ambiente GOMAXPROCS.(1)
Per capire meglio la potenzialità si pensi a un classico calcolatore in cui vengono
eseguite più applicazioni che concorrono tra di loro per l’ottenimento di dei core
di uno o più CPU. Se la nostra applicazione Go non necessità di parallelismo
è giusto diminuire il valore di GOMAXPROCS permettendo quindi alle altre
applicazioni di concorrere con un applicazione in meno.
Impostando al massimo il numero di core da utilizzare si può pensare di poter
accelerare l’esecuzione generale del programma, ma non sempre è così: se un
programma dedica più spazio alla comunicazione e alla sincronizzazione rispetto
alla parallelizzazione, questo comporterà un degrado in termini di prestazione
in quanto si utilizzeranno più thread del sistema operativo e il loro passaggio di
dati comporta un dispendioso cambio di contesto. (9)
Dunque bisogna valutare il tipo di operazione che effettuano le varie gor-
outine che compongono il programma e decidere di conseguenza. Nel caso del
progetto della Chat, in entrambi i moduli Chat-Server e Chat-Client ci sono
molte attività di comunicazione. Quindi aumentando il numero di core da uti-
lizzare comporterebbe a un degrado prestazionale.
for {
connection, _ := listener.Accept()
client := client.NewClient(&connection, chain, removeUser)
server.clients = append(server.clients, client)
19
go client.ListenForPackets()
}
(10)
20
creare molte goroutine e a utilizzare i channel sia per la comunicazione che per
la sincronizzazione.
Come detto in precedenza il modello di concorrenza di Go si basa sul con-
cettonon comunicare condividendo la memoria, ma condividere la memoria co-
municando e che viene rappresentato dall’immagine sottostante:
(11)
I channel vengono usati quindi sia come metodo di comunicazione che metodo
di sincronizzazione fra le varie goroutine soprattutto grazie alla proprietà bloc-
cante che hanno sulle ultime. Ad esempio in queste righe di codice vediamo
come il channel chiamato messagesSentChannel permetta di scrivere il valore
all’interno di un altro channel chiamato messagesSent ogni qualvolta che viene
inviato un messaggio. Questo ci permette di creare una sorta di sincronizzazione
tra le goroutine in modo da gestire l’incremento della variabile in modo tale da
non creare un’inconsistenza dei dati che rappresentano, fino a quel momento,
tutti i messaggi inviati dai client:
//send_message.go
func (handler *SendMessageHandler) Handle(packet packet.Packet, user
*entity.User) (Response, error) {
if user == nil {
return nil, fmt.Errorf("handler need authentication")
}
if user.Username != packet.Body["username"] {
return nil, fmt.Errorf("logged user mismatch with username in
packet request")
}
publishMessageRequest := usecase.NewPublishMessageRequest(
packet.Body["text"],
user.Username,
)
err := handler.useCase.Handle(publishMessageRequest)
if err != nil {
return nil, err
21
}
handler.messagesSentChannel <- true \\ il channel viene messo a true
return NewPacketResponse(), nil
}
//server.go
func (server *Server) consumeMessagesSentChannel() {
for {
messageSent := <-server.messagesSentChannel
if messageSent {
server.messagesSent += 1 // incremento del valore del channel
fmt.Printf("Total messages sent: %d\n", server.messagesSent)
}
}
}
22
i.messages[message.Id().String()] = message // inserimento del nuovo
messaggio
}
// server.go
func (server *Server) Start() {
//operazioni varie
go server.consumeBroadcastChannel()
//operazioni varie
}
3.2.4 Prestazioni
Per concludere il capitolo concorrenza, si consideri il successivo paragone fatto
tra Go e Java. È stato scritto un codice in entrambi i linguaggi in grado di
effettuare una connessione TCP in grado di comunicare con il programma Chat-
Server tramite la porta :8000. Per ogni test sono stati, quindi, generati un
numero preciso di goroutine e thread. Per misurare i tempi di tutte le attività
23
concorrenti, sia in Go che in Java, è stato tenuto in considerazione il tempo di
inizio e di conclusione per poi ottenere una somma in grado di definire il tempo
totale.
//JAVA
public class Benchmark extends Thread{
static int numberOfThread = 256;
static long totalTime;
socket.close();
long elapsed = System.nanoTime()-start;
increment(elapsed);
System.out.println(elapsed);
}
catch(UnknownHostException ex) {
ex.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
}
sleep(5000);
System.out.println("total time: "+totalTime);
}
24
}
1 0,020260523
10 0,248550925
100 2,185914845
256 3,530972005
//GO
var numberOfGoroutine = 256
var wg sync.WaitGroup
var m sync.Mutex
var totalTime float64
func connTcp() {
wg.Add(1)
defer wg.Done()
var t1 = time.Now()
_, err := net.Dial("tcp", ":8000")
if err != nil {
fmt.Println(err)
return
}
m.Lock()
t := time.Since(t1).Seconds()
totalTime = totalTime + t
m.Unlock()
fmt.Println(t)
}
func main() {
for i := 0; i < numberOfGoroutine; i++ {
go connTcp()
}
wg.Wait()
time.Sleep(3 * time.Second)
fmt.Println("total time")
fmt.Printf("%v\n", totalTime)
}
1 0.000181727
10 0.002438719
100 0.047565707999999984
256 0.3963921749999999
Sono stati stati eseguiti 4 test con differenti numeri di attività concorrenti: 1,
10, 100 e 256. L’ultimo numero è stato scelto in quanto la JVM riesce a gestire
25
fino a un massimo di 256 thread. In ogni caso questi dati sono abbastanza utili
per mostrare una sostanziale differenza tra i due linguaggi. Difatti, per ogni
numero di attività concorrente, Go si dimostra essere di molto superiore a Java.
Ovviamente queste analisi di prestazione non mostrano un’assoluta verità, bensì
devono essere presi come un’indicazione di questo specifico caso. Questo per-
ché ci possono essere diverse soluzioni a un singolo problema, così come per
la concorrenza di Java. Questo, però, afferma ancora di più uno degli aspetti
fondamentali di Go, ovvero quello di essere un linguaggio prestante permet-
tendo al programmatore di evitare la ricerca di particolari abilità e, soprattutto,
conoscenze di programmazione.
3.3 OOP
Se venisse posto a diversi programmatori la domanda “Go è un linguaggio ori-
entato agli oggetti?”, molto probabilmente non ci sarebbe una risposta unanime
come nel caso venisse posta ad esempio per Java o per C. E molto probabilmente
le risposte potrebbero dipendere dal linguaggio in cui programmano, ovvero: chi
programma in C potrebbe rispondere che Go è orientato agli oggetti, mentre
il contrario per i programmatori Java(14). Questo perché Go non supporta le
classi e gli oggetti, bensì utilizza le strutture e le interfacce. Inoltre non rispetta
tutti i 4 principi della programmazione ad oggetti o meglio ancora li ha adattati
in base alle proprie necessità.
3.3.1 Incapsulamento
Per incapsulamento si intende la possibilità di incapsulare o rendere privato un
qualsiasi dato all’interno di un oggetto(15). Ma dato che che in Go gli oggetti
non esistono, questo concetto viene riadattato a livello package o subpackage
che comprende anche i singoli file. Quando viene importato un package o un
file locale, tutti i dati pubblici vengono acceduti in modo diretto, viceversa per
quelli privati. Non esiste, però, nessuna keyword public o private che rappresenti
tutto ciò, bensì per rendere pubblico un metodo o una variabile, in fase di
dichiarazione, è necessario porre la prima lettera del nome in maiuscolo. Tutto
ciò che non ha la prima lettera maiuscola è invece resa privata e quindi non
modificabile dall’esterno:
26
3.3.2 Ereditarietà e Composizione
L’ereditarietà è un principio che si verifica quando diverse classi presentano delle
logiche comuni che porta quindi alla definizione di una superclasse che racchiude
queste logiche che verranno ereditate delle sottoclassi(15).
Da molto l’ereditarietà viene visto come un approccio da evitare in quanto
provoca diversi problemi come il problema del diamante che si verifica quando le
ereditarietà create, viste con un’immagine, formano un rombo(16). Il successivo
schema ne è un esempio:
• B e C ereditano da A;
• D eredita sia da B che da C;
Dunque se c’è un metodo in A che B e C hanno sovrascritto e D non lo
sovrascrive, quale versione del metodo D eredita: quella di B o quella di C?
Già nel 1994 veniva discusso nel libro Gof quanto l’ereditarietà fosse un
principio da non utilizzare, preferendo invece la Composizione e ricercando in
essa il comportamento polimorfico.
In Go il problema muore in partenza in quanto non supporta l’ereditarietà
incitando il programmatore ad usare la composizione. Questo principio si basa
sul fatto che una classe contiene un’altra classe. Nel contesto Go ovviamente
non si parlerà di classi, bensì di strutture, le quali ci permettono di avere un
concetto analogo. Quindi se la struct B contiene una struct A, la prima sarà in
grado di utilizzare tutti metodi pubblici della seconda. Rileggendo le definizioni
di ereditarietà e composizione possiamo già notare una differenza di relazione
che intercorre tra gli esempi di A e B:
• B eredita A -> B è un A;
• B è composto da A -> B ha un A;
Nell’esempio successivo viene indicata una struttura PublicMessage che serve
per rappresentare un messaggio che viene ricevuti da tutti i client connessi;
un’altra struttura RoomMessage che serve per rappresentare un messaggio in-
viato in una stanza specifica e quindi a tutti i client connessi presenti in quella
stanza. RoomMessage contiene/ha un PublicMessage.
27
3.3.3 Polimorfismo
Il polimorfismo rappresenta il concetto di assumere diverse forme e ne esistono
due tipi, elencati tenendo in considerazione che sono descritti in un contesto
orientato agli oggetti
• Statico: si verifica quando un oggetto consta di diversi metodi aventi lo
stesso nome, ma con diversi parametri che differiscono tra loro per numero,
ordine o tipo.
Queste due funzioni servono per salvare in una struttura dati rispettivamente le
due entità Messagge e User. Sebbene abbiano i parametri che differiscono per
tipo, in questo caso si verificherebbe un errore in fase di compilazione in quanto
condividono lo stesso nome. Il modo per ovviare a questo problema è quello di
assegnare un ricevitore diverso per ogni metodo. Ovviamente se assegnassimo
lo stesso ricevitore genererebbe comunque errore.
28
break
}
}
29
func (p *PublicMessage) SentAt() time.Time {
return p.sentAt
}
func NewRoomMessage(
id uuid.UUID,
body string,
sender *User,
sentAt time.Time,
room *Room,
) *RoomMessage {
publicMessage := NewPublicMessage(id, body, sender, sentAt)
return &RoomMessage{publicMessage, room}
}
publicMessage := message.NewPublicMessage()
roomMessage := message.NewRoomMessage()
30
body := message.Tipe()
fmt.Printf(body)
}
Quindi dopo aver creato un messaggio pubblico e uno privato, questi vengono
inseriti in un’array di tipo Message. Si esegue quindi un ciclo su tutti gli elementi
di tipo Message e per ognuno di essi si esegue il metodo message.Type() e si
stampa il risultato. In questo modo possiamo verificare che l’elemento message
sfrutti il polimorfismo per invocare il Type() in base al tipo del dell’elemento
corrente durante l’iterazione. La stama sarà:
3.3.4 Astrazione
I concetti di astrazione permettono la creazione di una soluzione a un problema
ridondante. Gli stessi concetti sono rappresentabili pure in Go in quanto non
bisogna seguire in specifico pattern come ad esempio per l’incapsulamento.
type X struct {
a, b, c int
buf [256]byte
}
31
In Java, il campo buf richiederebbe una seconda allocazione e per accedervi un
secondo livello di indirizzamento. In Go, invece, il buffer è allocato in un singolo
blocco di memoria insieme alla struttura che lo contiene e non è richiesta alcun
indirizzamento. Questo design può avere prestazioni migliori oltre a ridurre il
numero di elementi noti al collezionista. Su larga scala può fare una differenza
significativa.(17)
3.5 Dipendenze
Il primo passo per rendere Go scalabile dal punto di vista delle dipendenze, è
stato il fatto di generare un errore in fase di compilazione per le dipendenze
definite, ma mai utilizzate. Se il file di origine importa un pacchetto che non
utilizza, il programma non verrà compilato. Questo garantisce che l’albero delle
dipendenze sia in linea con le tecnologie utilizzate e che non abbia dipendenze
inutile. Ciò, a sua volta, garantisce che nessun codice aggiuntivo venga compi-
lato durante la compilazione del programma, riducendo al minimo il tempo di
compilazione.
C’è un altro passaggio, questa volta nell’implementazione dei compilatori,
che va ancora oltre per garantire l’efficienza. Si consideri un programma Go con
tre pacchetti e questo grafico delle dipendenze:
32
ancora più scalabile dal punto dei tool utilizzabili a differenza di Java che ha
bisogno gestori delle dipendeze come Maven o Ant. In Go c’è un solo file da
aprire per importare i pacchetti e framework. Inoltre, l’approccio a file singolo
significa che i dati di esportazione non possono mai essere obsoleti rispetto al
file oggetto.
Un’altra caratteristica del grafico delle dipendenze Go è che non ha cicli.
Sebbene occasionalmente siano utili, le importazioni circolari introducono prob-
lemi significativi su vasta scala. Richiedono al compilatore di gestire set più
grandi di file sorgente contemporaneamente, il che rallenta le build in modo in-
crementale. La mancanza di importazioni circolari provoca fastidio occasionale,
ma mantiene pulito l’albero delle dipendenze, costringendo a una chiara demar-
cazione tra i pacchetti. Come per molte delle decisioni di progettazione in Go,
costringe il programmatore a pensare prima a un problema su larga scala che, se
lasciato a un momento successivo, potrebbe non essere mai affrontato in modo
soddisfacente.
Inoltre bisogna sempre ricordare che può essere meglio copiare un piccolo
codice che inserire una grande libreria per una funzione. L’igiene delle dipen-
denze ha la meglio sul riutilizzo del codice.(17)
33
• la mancanza di una keyword per implementare un’interfaccia
• la mancanza dell’ereditarietà
• e molte altre
Il fatto che fornisca un garbage collector permette di eliminare i tempi di
gestione della memoria e possibili errori compiuti dal programmatore come ad
esempio la mancata deallocazione di un elemento in memoria.
Ricordiamo inoltre come Go inciti a uno stile di programmazione pulito come
la mancanza dell’ereditarietà e l’uso di più attività concorrenti, le goroutine, le
quali hanno un metodo di comunicazione semplice come i channel.
Insomma Go è veramente facile da usare, nonostante i difetti e le mancanze
che ha avuto nel corso degli anni come i Generics, introdotti solo recentemente
con la versione di Go 1.18 nel marzo 2022. Abbiamo visto poi la mancanza
dell’overloading dei metodi e la mancanza delle eccezioni portando al program-
matore a pensare e quindi a gestire prima gli errori portando molto spesso ad
avere un codice lungo.
Il titolo di questo sotto-capitolo è ripreso dall’articolo di uno dei creatori di
Go, Rob Pike. Egli afferma che le caratteristiche di Go avrebbero dovuto portare
molti programmatori C++ ad usare il neolinguaggio, dato che in Google che si
programma C++. Questo non accadde in quanto cambiare linguaggio avrebbe
reso vano ottenimento delle difficili abilità ottenute nel corso degli anni. Da
subito, invece, sono arrivati programmatori Python e Ruby i quali non hanno
dovuto rinunciare a troppe caratteristiche peculiare, andando alla ricerca di un
linguaggio in grado di offrire ottime prestazioni e facilità della gestione della
concorrenza.(18)
4 Conclusioni
All’inizio del percorso di stage non avevo nessuna conoscenza di Go e, soprat-
tutto, non conoscevo in chiaro le sue caratteristiche fondamentali. Però non è
stato per niente difficile approcciarmi a questo nuovo linguaggio, anzi. Difatti il
suo apprendimento da parte mia è stato veramente rapido, parlando addirittura
di giorni. Questo ha, inoltre, portato a un rapido sviluppo della Chat, crean-
domi talvolta delle scorciatoie soprattutto per quanto riguarda il problema della
concorrenza.
Ovviamente questo non lo rende il linguaggio migliore al mondo. Infatti, Go
pecca per alcune problematiche citate nel capitolo 3.6 ed altre sollevate dalla
cummunity del linguaggio stesso.
In futuro desidero continuare con lo studio del linguaggio come la gestione
degli errori, i generics e tutte le successive implementazioni portate dalle nuove
versioni.
34
References
[1] Donovan, A. & Kernighan, B. W. The go programming language. In The
Go Programming Language (2015).
35
[14] Copes, F. Is go object oriented (2017). URL https://flaviocopes.com/
golang-is-go-object-oriented/. Last accessed on 2022-05-06.
[15] Tramontana, G. La programmazione ad oggetti spiegata in modo sem-
plice (2017). URL https://www.gianlucatramontana.it/2017/12/05/
la-programmazione-ad-oggetti-spiegata-in-maniera-semplice/.
Last accessed on 2022-05-06.
[16] Wikipedia contributors. Multiple inheritance — Wikipedia, the free en-
cyclopedia. https://en.wikipedia.org/w/index.php?title=Multiple_
inheritance&oldid=1083759773 (2022). [Online; accessed 7-May-2022].
36