#howtodev - Introduzione alla programmazione in Go
Introduzione
Go è un linguaggio di programmazione semplice e facile, moderno e prestante.
Requisiti
Per poter meglio comprendere la guida è preferibile la conoscenza dei seguenti argomenti:
- operatori aritmetici per l’esecuzione di calcoli di base (addizione, moltiplicazione, etc…)
- matematica booleana (and, or, not, etc…)
- uso del sistema binario
- codifiche ASCII e Unicode
- concetto di processo, thread e green thread
- differenza tra concorrenza e parallelismo
Questi requisiti non sono tutti essenziali per la comprensione di questa guida, ma ne facilitano la comprensione e limitano la quantità di informazioni da ricercare a mano.
Obiettivi
L’obiettivo di questa guida è portare il lettore da zero ad un livello di competenze tale che possa continuare i propri studi in modo autonomo.
Gli argomenti coperti saranno:
- primi semplici programmi
- spiegazione della sintassi di base
- tipi base e alcuni tipi avanzati
- operazioni con i tipi di base
- funzioni
- routine e thread
- metodi e tipi complessi
- flussi di controllo e cicli
- prime esercitazioni nel linguaggio.
Subito dopo la spiegazione di base copriremo la creazione di un breve programma
Preparazione dell’ambiente di lavoro
È possibile lavorare con Go in vari modi:
Installazione locale (consigliata)
Scaricare l’implementazione ufficiale di Go tramite la nostra guida o il gestore dei pacchetti della propria distribuzione
Debian/Ubuntu
Si può installare Go tramite apt con:
apt install go
RedHat/Fedora e derivate
Go può essere installato su ogni distro che usi dnf con:
dnf install golang
Alcune derivate RedHat però potrebbero ancora usare il vecchio yum. Se fosse è il caso allora il comando è:
yum install golang
Arch e derivate
Su Arch è possibile installare Go con pacman usando questo comando:
pacman -S go
Void e derivate
Si può usare xbps per installare Go con:
xbps-install go
Ambiente web
Se si desidera scrivere ed eseguire codice Go dal browser, è disponibile il Go playground ufficiale (alquanto limitato) o il sito di Replit (un po’ meno limitato, mi permetto di consigliare quest’ultimo tra le due).
Scheletro del programma
Un programma in Go deve seguire uno schema preciso. Analizziamo un esempio di hello world:
package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
- Definiamo il pacchetto principale con
package main
- Importiamo il pacchetto
fmt
(fmt sta per format) - Definiamo la funzione principale con
func main() {
- Richiamiamo la funzione
Println
dal pacchettofmt
per stampare a schermo la frase “Hello World!” - Chiudiamo il blocco di codice della funzione con
}
Ora che ci siamo fatti un’idea di quello che sta scritto, analizziamo tutto nel dettaglio.
Commenti (stile C-like)
Un commento delimita una porzione di testo che non verrà considerata dal compilatore
Esistono due tipi di commenti:
Commenti in linea
Ogni cosa dopo il delimitatore //
verrà ignorata. Questo genere di commenti è consigliato se si usa una sola linea di commento (piccole specifiche)
fmt.Println("qualcosa") // Questo verrà ignorato
// Anche questo
// fmt.Println("Ciao") questa riga non verrà mai eseguita
Commenti in blocco
Ogni cosa contenuta nei delimitatori /*
*/
sarà ignorata. Questo genere di commenti è consigliato se il commento è particolarmente lungo e articolato, tanto da dover essere disposto su più righe
/*
Tutto questo
verrà ignorato
*/
fmt.Println("ciao" /*, "mondo"*/)
/* La parola mondo verrà ignorata */
Pacchetti
I programmi in Go si suddividono in pacchetti.
La parola chiave package
è obbligatoria e serve per definire un pacchetto.
Il pacchetto chiamato “main” è quello principale, dal quale comincia l’esecuzione del programma.
Import
All’interno del proprio pacchetto sarà possibile importarne altri grazie alla parola chiave import
. È possibile importare sia usando le parentesi ()
che scrivendo direttamente il nome del pacchetto circondato da doppi apici ""
.
import (
"fmt"
"strings"
"net/tcp"
)
import "fmt"
import "strings"
import "net/tcp"
Tipi
Un tipo è come si può intuire dal nome, il tipo di dato con cui stiamo lavorando.
Possiamo suddividere i tipi in varie categorie e sottocategorie.
Numerici
Numeri interi (integer) segnati
Esistono vari tipi numerici interi, essi possono avere varie dimensioni. Quelli che vedremo ora sono dotati di bit del segno
Un int8
ad esempio occuperà 8 bit o un byte. Occupando 8 bit il raggio di valori che potrà assumere questo tipo spazia da -128
a 127
poiché il massimo numero ottenibile con 8 bit è 256
(128+127
fa 255
, aggiungendo anche 0
arriviamo a 256
possibili valori).
Di seguito una tabella dei tipi completa:
Tipo | Raggio |
---|---|
int8 | -128 a 127 |
int16 | -32768 a 32767 |
int32 | -2147483648 a 2147483647 |
int64 | -9223372036854775808 a 9223372036854775807 |
int | Dipendente dall’implementazione (generalmente uguale a int32 ) |
Numeri interi (integer) non segnati
Se non si dovesse avere bisogno di numeri minori di zero allora è possibile riservare quello spazio per poter esprimere un valore più alto. Per esempi il tipo uint8
occuperà 8 bit e sarà non segnato (u = unsigned), il suo raggio di valori varia tra 0
a 255
, potendo assumere quindi sempre un totale di 256
valori differenti.
Ecco una tabella completa anche degli interi non segnati:
Tipo | Raggio |
---|---|
uint8 | 0 a 255 |
uint16 | 0 a 65535 |
uint32 | 0 a 4294967295 |
uint64 | 0 a 18446744073709551615 |
uint | Dipendente dall’implementazione (generalmente uguale a uint32 ) |
Numeri in virgola mobile (floating) e numeri complessi (complex)
Per il bene della semplicità non scenderò nei dettagli qui, ma per esprimere numeri con la virgola possiamo usare i tipi float32
e float64
.
È anche possibile utilizzare numeri complessi grazie al tipo complex
.
Booleani
Nulla di complicato, sono tipi che possono assumere due valori, vero (true
) e falso (false
).
Casting dei tipi
Per convertire un tipo in un altro è possibile effettuare il casting.
uint8(10) // Questo 10 non tipizzato verrà convertito in uint8
NB: I valori non tipizzati sono valori il cui tipo non è ancora stato deciso
Altri tipi numerici
Alcuni tipi possono assumere anche altri nomi poiché vengono usati per rappresentare diversi tipi di dati.
Ecco alcuni esempi:
Tipo | Equivalente | Uso comune |
---|---|---|
byte | uint8 | Esprimere caratteri ASCII o semplicemente byte |
rune | uint32 | Esprimere caratteri Unicode |
Rune
Rune è utile per lavorare con Unicode, ed è il tipo che si usa di default per i caratteri in Go.
'a' // Uguale a 97, ma è una runa
'\n' // Uguale a 10, ma è una runa
'a' + 1 // Questo codice è valido
'a' + '\n' // Anche questo
Byte
Tramite casting è possibile lavorare con i byte come se fossero semplici numeri, dato che si tratta semplicemente di alias.
È anche possibile esprimere caratteri come byte racchiudendo il carattere tra singoli apici ''
effettuando il casting a byte.
Esempio:
byte('a') // Uguale a 97
byte('\n') // Uguale a 10
byte('a') + 1 // Questo codice è valido
byte('a') + '\n' /* Anche questo
('\n' verrà automaticamente convertito in byte) */
Tipi composti
Il linguaggio possiede alcuni tipi composti da più tipi semplici.
Array
Un insieme di valori può essere raccolto in un array, cioè una lista a lunghezza fissa.
Il tipo di un array si esprime con [n]t
dove n
è la lunghezza e t
è il tipo dei valori contenuto in esso.
[10]int
conterrà 10 numeri interi.
È possibile richiamare un singolo valore da un array specificandone l’indicine tra parentesi quadre []
mioArray[0] = 10 // Ora il primo valore di mioArray sarà uguale a 10
NB: Gli indici iniziano sempre da 0
Slice
Se un array ha una grandezza predefinita, quella di una slice è dinamica.
Quest’ultima non è altro che una referenza ad un array già esistente per rendere più semplice la vita dello sviluppatore.
Le slice si esprimono in modo simile ad un array, []t
dove t
è il tipo dei valori contenuti in essa. È anche possibile inizializzare delle slice con la funzione make
, così da specificarne una lunghezza iniziale e la capacità dell’array sottostante (che sarà solo un base della slice, essa non imporrà reali limiti di capienza).
make([]int, 10) // Slice di int con lunghezza 10
make([]int, 0, 10) // Slice di int con lunghezza 0 e capienza 10
Si può ottenere una slice da un array usando due valori separati da :
al posto dell’indice.
mioArray[0:9] // Sarà una slice che prende i primi 10 valori di mioArray
(È anche possibile ottenere una slice di una slice in questo stesso modo)
Stringhe
Per esprimere del testo ci basterà usare una stringa.
Possono essere rappresentate in due modi
-
Con i doppi apici
""
. Verrà effettuato l’escape (quindi la “traduzione”) di caratteri come il newline (\n
) per andare a capo o il tabulatore (\t
). -
Con gli accenti gravi ` ` . Non verrà effettuato l’escape di caratteri speciali, tuttavia sarà possibile esprimere la stringa su più righe.
"Testo\nA capo"
`Testo
A capo`
// Le due sono equivalenti
È possibile ottenere delle stringe partendo da dagli array o slice di byte o rune:
string([]byte{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100})
string([]rune{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100})
// Queste due stringhe equivalgono a "hello world"
Vale anche il processo inverso:
[]byte("hello world")
[]rune("hello world")
/*
Entrambe varranno:
104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100
*/
Strutture
Le strutture sono essenzialmente un aggregato di attributi (valori) e metodi (funzioni).
È possibile definirle specificando nome della strutture, nome degli attributi e tipo degli attributi:
type nomeDellaStruttura struct {
attributo1 unTipo
attributo2 unTipo
attributo3 unAltroTipo
}
Tipi puntatori
Un puntatore è un tipo speciale, esso punta direttamente all’indirizzo di memoria di una variabile, dandoci quindi la possibilità di lavorare sullo stesso valore da posizioni differenti.
Un puntatore viene rappresentato scrivendo il tipo a cui punta preceduto da un asterisco *
(Per esempio *int
).
Se non puntano ad un indirizzo di memoria essi possono assumere un valore nullo nil
Operatori
Operatori aritmetici
Come intuibile dal nome, essi consentono di eseguire operazioni aritmetiche.
Eccone un elenco:
Simbolo | Operatore | Descrizione |
---|---|---|
+ |
Addizione | Addiziona due numeri |
- |
Sottrazione | Sottrae due numeri |
* |
Moltiplicazione | Moltiplica due numeri |
/ |
Divisione | Divide due numeri |
% |
Modulo (o resto) | Ritorna il resto della divisione tra due numeri |
E ora dei piccoli esempi per spiegare gli operatori di incremento e decremento:
a := 1
a++ // adesso a vale 2
a-- // adesso a vale 1
a := 1
a += 2 // adesso a vale 3
a -= 3 // adesso a vale 0
Operatori Logici
Essi permettono di effettuare confronti tra più valori booleani, eccone una lista:
Simbolo | Operatore | Descrizione |
---|---|---|
&& |
E (and) | Controlla se due booleani entrambi sono veri |
|| |
O (or) | |
! |
Non (not) | Inverte un booleano |
Ognuna di queste operazioni ritorna sempre un booleano, indipendentemente dai valori confrontati.
Operatori relazionali
Grazie ad essi possiamo effettuare confronti tra più valori, la lista comprende:
Simbolo | Operatore | Descrizione |
---|---|---|
== |
Uguale | Controlla se due valori sono uguali |
!= |
Diverso | Controlla se due valori sono differenti |
> |
Maggiore | Controlla se un valore è maggiore di un altro |
< |
Minore | Controlla se un valore è minore di un altro |
>= |
Maggiore o uguale | Controlla se un valore è maggiore o uguale ad un altro |
<= |
Minore o uguale | Controlla se un valore è minore o uguale ad un altro |
Come nel caso degli operatori logici, anche questi ritornano sempre un booleano, indipendentemente dai valori confrontati.
Operatori bit per bit
Tali operatori consentono di effettuare operazioni sui singoli bit.
Questo implica che ogni numeri passatogli verrà convertito in base due.
Simbolo | Operatore | Descrizione |
---|---|---|
& |
E bit per bit (bitwise and) | Esegue and su ogni singolo bit |
| |
O bit per bit (bitwise or) | Esegue or su ogni singolo bit |
^ |
XOR bit per bit (bitwise XOR) | Esegue xor su ogni singolo bit |
&^ |
NOR bit per bit (bitwise NOR) | Esegue nor su ogni singolo bit |
<< |
Spostamento binario verso sinistra (left shift) | Sposta ogni bit verso sinistra di x volte |
>> |
Spostamento binario verso destra (right shift) | Sposta ogni bit verso destra di x volte |
Ognuno di essi ritorna sempre un numero.
NB: Per x
si intende il secondo valore dell’operazione
Operatori d’assegnazione
Essi consentono di effettuare operazioni ed assegnare variabili con un solo operatore.
Simbolo | Operatore | Descrizione |
---|---|---|
= |
Assegnazione semplice | Assegna x ad una variabile |
++ |
Assegnazione con incremento | Incrementa una variabile di 1 |
+= |
Assegnazione con incremento di x | Incrementa una variabile di x |
-- |
Assegnazione con incremento | Decrementa una variabile di 1 |
-= |
Assegnazione con decremento di x | Decrementa una variabile di x |
*= |
Assegnazione moltiplicazione | Assegna ad una variabile il risultato della sua moltiplicazione con x |
/= |
Assegnazione divisione | Assegna ad una variabile il risultato della sua moltiplicazione con x |
%= |
Assegnazione modulo | Assegna ad una variabile il resto della sua divisione con x |
<<= |
Assegnazione spostamento binario verso sinistra | Assegna ad una variabile il suo valore con i bit spostati verso sinistra di x posti |
>>= |
Assegnazione spostamento binario verso destra | Assegna ad una variabile il suo valore con i bit spostati verso destra di x posti |
&= |
Assegnazione AND bit per bit | Assegna ad una variabile suo valore dopo un’operazione AND bit per bit |
|= |
Assegnazione OR bit per bit | Assegna ad una variabile suo valore dopo un’operazione OR bit per bit |
^= |
Assegnazione XOR bit per bit | Assegna ad una variabile suo valore dopo un’operazione XOR bit per bit |
Variabili
È possibile conservare dei valori in dei contenitori chiamati variabili.
Variabili locali
Una variabile locale è utilizzabile solo all’interno del proprio scopo, cioè l’area in cui è stata definita.
Esse possono essere definite in due modi:
Dichiarazione esplicita
Essa consiste nel dichiarare il tipo di una variabile a mano prima di assegnarne un valore.
var primaVariabile int32 = 1
var secondaVariabile int64 = 1
Dichiarazione implicita o inferenza del tipo
Ossia il rilevamento automatico di quest’ultimo nella fase di dichiarazione della variabile.
Per questo possiamo usare una “short variable declaration” (letteralmente “dichiarazione corta di variabile”)
unaVariabile := 1 // Verrà assunto il tipo int
Variabili globali
Le variabili globali sono accessibili in ogni parte del programma. Esse possono essere solo definite fuori da una funzione in due modi:
Dichiarazione esplicita
Pressoché identica alla dichiarazione esplicita di una variabile locale
var primaVariabile int32 = 1
var primaVariabile int64 = 1
// oppure
var (
primaVariabile int32 = 1
secondaVariabile int64 = 1
)
Dichiarazione implicita o inferenza del tipo
A differenza delle variabili locali qui non sarà necessario usare il :=
, ma var
sarà sempre richiesto.
var unaVariabile = 1 // Verrà assunto il tipo int
Puntatori a variabili
Per prendere l’indirizzo di memoria di una variabile così da creare un puntatore ad essa possiamo usare la e commerciale (&
):
miaVar := 10 // il tipo è int
mioPuntatore := &miaVar // il tipo è *int
Se proviamo però a lavorare con un puntatore noteremo che non è altro che un indirizzo di memoria e non il valore al quale stiamo puntando.
Per poter interagire con esso dobbiamo prima deferenziarlo usando l’asterisco (*
):
miaVar := 10 // il tipo è int
mioPuntatore := &miaVar // il tipo è *int
unAltraVar := *mioPuntatore // il tipo è int
Nell’esempio qui sopra deferenziando mioPuntantore
abbiamo ottenuto il valore originale di miaVar
.
Tramite la deferenziazione possiamo anche modificare quel valore:
miaVar := 10 // il tipo è int
mioPuntatore := &miaVar // il tipo è *int
*mioPuntatore = 3 // miaVar ora è uguale a 3
Attenzione però, deferenziando un puntatore nullo (nil
) otterremo un errore che fa andare in panico (e quindi generalmente in crash) il programma.
var mioPuntatore *int = nil
mioPuntatore // vale nil
*mioPuntatore // manda in crash il programma
Costanti
Le costanti così come le variabili, solo che non posso assumere valori come array e slice.
Esse vengono dichiarate con la parola chiave const
.
const primaCostante int64 = 10
const (
secondaCostante = 20
terzaCostante int8 = 30
)
Funzioni
In Go è possibile definire funzioni. Una funzione è un pezzo di codice riutilizzabile che può accettare parametri e ritornare risultati.
Blocchi di codice
Un blocco di codice non è altro che una porzione di istruzioni raccolta in delle parentesi graffe.
{
// Qui va il codice da eseguire
}
Essi delimitano anche uno scopo, argomento che verrà approfondito in seguito.
Definizione di una funzione
Per definire una funzione in Go possiamo usare la parola chiave func
L’esempio che segue mostra come definire una funzione vuota chiamata “miaFunz”
func miaFunz() {
}
Accettare parametri
Una funzione può accettare uno o più parametri definendoli col loro nome e tipo:
In questo esempio possiamo notare una funzione con due parametri:
parametroUno
di tipostring
parametroDue
di tipoint
func miaFunz(parametroUno string, parametroDue int) {
}
Se si hanno più parametri dello stesso tipo è possibile anche scrivere una sola volta il tipo, come in questo esempio:
func miaFunz(parametroUno, parametroDue string) {
}
Se volessimo modificare un parametro direttamente di una funzione senza bisogno di ritornare potremmo semplicemente usare un puntatore.
func miaFunz(mioParametro *int) {
*mioParametro = 1 // deferenziamo mioParametro prima di modificarlo
}
Ritornare un risultato
Una funzione può ritornare uno o più risultati semplicemente scrivendone i tipi prima dell’apertura del blocco di codice e usando la parola chiave return
.
func ritornaUnValore() string {
return "valore"
}
func ritornaDueValori() (string, string) {
return "valore1", "valore2"
}
NB: Racchiudere sempre i tipi in delle parentesi tonde quando si vuole ritornare più valori
Richiamare una funzione
Per richiamare una funzione basta scriverne il nome seguito da delle parentesi tonde in cui andranno inseriti gli eventuali argomenti.
Definiamone una che prenda due numeri e ne ritorni la somma così:
func somma(n, m int) int {
return n + m
}
A questo punto possiamo semplicemente richiamarla con
somma(10,5) // Ritornerà 15
Se volessimo passare ad una funzione un puntatore ci basterebbe usare la e commerciale (&
) come precedentemente spiegato.
Ecco l’esempio di una funzione che raddoppia un numero e invece di ritornare il risultato cambia direttamente il valore della variabile originale:
func doppio(n *int) {
*n = *n + *n
}
miaVar := 10
doppio(&n) // Adesso miaVar vale 20
Metodi
In Go i metodi non sono altro che un modo più semplice di scrivere una funzione che prende in input un tipo creato dall’utente.
Si definiscono come normali funzioni, ma prima dell’identificatore bisogna specificare il tipo tra parentesi in questo modo:
type mioTipo struct {
// contenuto
}
func (nomeParametro mioTipo) mioMetodo() {
// contenuto
}
Se vogliamo modificare i contenuti di una struttura o semplicemente risparmiarci un’allocazione quando richiamiamo il metodo, possiamo passare il tipo come puntatore.
type mioTipo struct {
// contenuto
}
func (nomeParametro *mioTipo) mioMetodo() {
// contenuto
}
Chiusure
Go supporta le cosiddette “chiusure”, ossia delle funzioni anonime che possono lavorare con delle variabili accessibili dallo stesso scopo nel quale sono esse sono state definite.
La definizione di una chiusura è identica a quella di una normale funzione, ma non richiede la scrittura dell’identificatore.
miaVar := 10
func() {
miaVar = 2
}() // le () finali servono per eseguirla subito dopo
// ora miaVar vale 2
È anche possibile creare delle funzioni che accettino delle chiusure (o funzioni normali) come parametro. Basta scrivere la firma della funzione al posto del tipo.
Ecco un esempio dove una funzione ne accetta un’altra, la esegue, e ne elabora il risultato:
func aggiungiUno(funz func() int) int {
return funz() + 1
}
aggiungiUno(
func() int {return 10}, // la virgola si usa sempre in una lista di argomenti a cascata
)
Funzione main
La funzione main
è obbligatoria in un pacchetto main
ed è la prima ad essere eseguita a meno che non sia presente una funzione init
.
Goroutine
Questa parte potrebbe risultare un po’ più complessa e ma comunque facente parte integrante del linguaggio, per cui cercherò di farla più semplice possibile e senza scendere in dettagli.
Una goroutine è un green thread. La funzione main
gira nella goroutine principale, ma possiamo anche crearne di nostre in modo semplicissimo con la parola chiave go
Per esempio potremmo fare:
go primaFunzione()
go secondaFunzione()
go func() { // chiusure
}()
e tutte e tre le funzioni verranno eseguite in modo concorrente.
Quando una goroutine termina l’esecuzione, anche le figlie verranno forzate ad interromperla, per questo normalmente si usano metodi di blocco come attese, waitgroup, e canali.
Tali casi non verranno trattati da questa guida che vuole fornire solo un’infarinatura delle goroutine.
Controllo di flusso
If-else
Si può usare il costrutto if per eseguire codice solo se una determinata condizione è soddisfatta.
-
if
viene usato come primo controllo ed eseguirà il codice se la condizione è soddisfatta. -
if else
è opzionale e consente di effettuare altri controlli con condizioni differenti (a patto che quelli precedenti non siano andati a buon fine). -
else
è anche esso non obbligatorio e va posto come ultimo controllo. Indica una porzione di codice che verrà eseguita se nessuna condizione è soddisfatta.
n := 1
a := 2
if a == 2 {
// questo verrà eseguito
}
if n > 2 && a < n {
// questo non verrà eseguito
} else if n < 0 {
// neanche questo
} else {
// questo sì
}
Switch
Uno switch consente di effettuare un controllo sul valore di una singola variabile, e quindi non su una condizione qualsiasi.
Ecco un esempio di tale controllo:
n := 10
switch n {
case 1:
// non verrà eseguito
case 2, 3:
// non verrà eseguito
default:
// verrà eseguito
}
Default è opzionale e viene eseguito dal momento in cui nessun caso si sia avverato.
È anche possibile usare la parola chiave falltrough
per far continuare il controllo dopo l’esecuzione del caso. Per bloccare un falltrough
è possibile usare un break
n := 10
switch n {
case 10:
// verrà eseguito
falltrough
case 11:
// verrà eseguito lo stesso
}
Goto
Utile per saltare direttamente ad un determinato punto nel codice segnato da un’etichetta (label
).
L’esempio qui sotto continuerà a stampare all’infinito la frase “Hello World”:
testo: := "Hello World"
miaEtichetta:
fmt.Println(testo) // stampa la variabile testo
goto miaEtichetta
Per motivi di sicurezza non è possibile usare goto
per andare in un altro scopo o saltare la definizione di una variabile
Cicli
Generalmente nella programmazione sono presenti vari tipi di cicli, ma in Go esiste solo la parola chiave for
che assume diversi comportamenti in base al contesto.
For infinito/loop
Per ripetere un pezzo di codice all’infinito è possibile racchiuderlo in un semplice for
, senza specificare condizioni o altro.
for {
// questo codice verrà eseguito all'infinito
}
È possibile uscire da tale ciclo con un break
for {
// questo codice verrà eseguito una sola volta
break // questo break esce dal ciclo
}
È possibile anche saltare un singolo passaggio de ciclo invece di arrestarlo del tutto con la parola chiave continue
.
Nonostante introdotte in questa sezione, le parole chiavi break
e continue
sono disponibili in ogni tipo di ciclo for
.
For condizionale/while
Si può far andare avanti un ciclo finché una data condizione viene rispettata.
Nell’esempio sotto il ciclo va avanti finché n
non diventa pari a 10
:
n := 0
for n < 10 {
n++ // questo codice verrà eseguito 10 volte
}
For classico/for
Se si conosce già il numero di iterazioni è possibile creare un ciclo for che vada avanti per un tempo prestabilito.
La sintassi in questo caso può essere scomposta con:
for
definizione di contatore;
condizione;
incremento
for i := 0; i < 10; i++ {
// questo ciclo verrà eseguito 10 volte
}
For iterativo/range
Se si ha qualcosa su cui iterare come una slice o un array, è possibile farlo grazie alla parola chiave range
.
lista := [10]int{1,2,3,4,5,6,7,8,9,10}
for i, v := range lista {
// questo codice verrà eseguito 10 volte
}
Laddove i è l’indice, cioè numero dell’iterazione (partendo da zero) e v è il valore che stiamo prendendo dalla lista (in pratica v == lista[i]
).
Esercitazione
Adesso proveremo a creare un breve programma che genera un numero casuale all’utente e gli chiede di inserirli in input.
Attenzione
-
Il Go playground non supporta l’input inserito dall’utente.
-
In ambienti di test come il Go playground o repl it la generazione di numeri casuali potrebbe non funzionare.
Scheletro
Come prima cosa scriviamo lo scheletro del programma
package main
import (
)
func main() {
}
E ora iniziamo a condire il tutto.
Import dei moduli
Importiamo i moduli a noi necessari
-
fmt
per stampare e prendere l’output -
math/rand
per generare il numero casuale -
time
per stabilire il seme di generazione del numero casuale
Impostiamo i limiti
Decidiamo quali limiti vogliamo per i nostri numeri casuali. In questo caso voglio che vengano generati da 1
a 10
, quindi creerò due costanti:
const (
min = 1
max = 10
)
Creiamo una funzione
Adesso creiamo la funzione che genererà numeri casuali:
func generaNumer() int {
return (rand.Intn(max-min) + min)
} // genera un numero da min a max e lo ritorna
Nel main
Andiamo a scrivere il codice necessario all’interno della funzione main
Impostiamo il seme di generazione
rand.Seed(time.Now().UnixNano())
-
time.Now()
ci ritorna l’ora locale -
.UnixNano()
la esprime in tempo unix in nanosecondi (ossia tempo passato dal 01/01/1970 ad oggi in nanosecondi) -
rand.Seed()
accetta un valore numerico e lo usa come seme per generare numeri casuali
Attenzione: il seme va stabilito solo una volta in tutto il programma
Dichiariamo una variabile vuota
In essa l’utente potrà inserire la propria scelta:
var scelta int
Chiediamo all’utente di inserire un input
Tramite fmt.Print
possiamo stampare a schermo una stringa che non termini con una nuova riga:
fmt.Print("Indovina un numero da 1 a 10: ")
Inviamo input alla variabile vuota
Con fmt.Scan
possiamo inviare un input numerica alla variabile tramite un puntatore ad essa:
fmt.Scan(&scelta)
Facciamo il confronto
Richiamiamo la funzione generaNumero
e confrontiamola con la variabile scelta
if scelta == generaNumero() {
} else {
}
Comunichiamo l’esito all’utente
Possiamo usare fmt.Println
per stampare a schermo una stringa che termini con una nuova riga (a differenza di fmt.Print
)
if scelta == generaNumero() {
fmt.Println("Complimenti, hai indovinato!")
} else {
fmt.Println("Peccato, hai perso...")
}
Risultato finale
Se tutto è andato per il verso giusto alla fine si otterrà un programma simile a questo
package main
import (
"fmt"
"math/rand"
"time"
)
const (
min = 1
max = 10
)
func generaNumero() int {
n := rand.Intn(max-min) + min
return n
}
func main() {
rand.Seed(time.Now().UnixNano())
var scelta int
fmt.Print("Indovina un numero da 1 a 10: ")
fmt.Scan(&scelta)
if scelta == generaNumero() {
fmt.Println("Complimenti, hai indovinato!")
} else {
fmt.Println("Peccato, hai perso...")
}
}
Ora puoi eseguire il tutto con go run
e compilare con go build
se hai installato il compilatore, altrimenti usa l’apposito tasto dagli ambienti web.
Conclusioni
Questo è quanto. Non era poi così difficile, vero?