Iniziamo con una piccola introduzione a Go (o Golang). Go è stato realizzato dagli ingegneri di Google: Robert Griesemer, Rob Pike e Ken Thompson. È un linguaggio compilato. La prima versione fu rilasciata come open source nel Marzo 2012.
“Go è un linguaggio di programmazione open source che semplifica la creazione di software semplice, affidabile ed efficiente”.
Ogni linguaggio ha i suoi specifici modi per risolvere i problemi. I programmatori possono impiegare molto tempo nel capire come risolverli.
Go, d’altra parte, crede in poche, sicure, funzionalità. Un solo modo giusto per risolvere un problema.
Ciò salva ogni sviluppatore dallo scrivere un’enorme quantità di codice poco mantenibile.
Non ci sono funzionalità “espressive” in Go come ad esempio maps e filtri.

Iniziamo!
Go è costituito da “packages”, che avrai incontrato sicuramente in altri linguaggi. Il package main dice al compilatore di Go che il programma è compilato ed eseguibile. È l’entry point dell’applicazione ed è definito come:
package main
Ora andiamo avanti scrivendo un semplice Hello World creando il file main.go nel workspace di Go.
Workspace
Il workspace di go è definito dalla variabile d’ambiente GOPATH.
Ogni codice che andrai a scrivere deve essere collocato all’interno di questo path. Go andrà a cercare ogni pacchetto all’interno della cartella riferita dalla GOPATH, o la GOROOT, che è impostata in automatico quando installiamo Go ed è la cartella in cui risiedono i file del linguaggio.
Impostiamo la GOPATH alla cartella che preferiamo. Per ora aggiungila nella cartella ~/workspace.
# export env export GOPATH=~/workspace # go inside the workspace directory cd ~/workspace
Crea il file main.go, con il seguente codice all’interno:
package main import ( "fmt" ) func main(){ fmt.Println("Hello World!") }
Nell’esempio sopra, fmt è un pachage nativo di Go che implementa le funzioni di I/O (Input/Output).
Per importare un pacchetto in Go basta usare la direttiva import. func main è il metodo principale che viene eseguito appena lanciata l’applicazione.
Iniziamo con l’eseguire questo file. Ci sono due modi per lanciare un comando Go. Come sappiamo, essendo un linguaggio compilato, dobbiamo effettuare la compilazione prima di eseguirlo.
> go build main.go
Questo comando creerà un file binario eseguibile chiamato main che ora possiamo eseguire con:
> ./main # Hello World!
C’è un altro modo più semplice. Digitando go run:
go run main.go # Hello World!
Note: Puoi provare il codice che trovi in questo articolo sul sito https://play.golang.org
Variabili
Le variabili in Go sono dichiarate esplicitamente. Una variabile può essere dichiarata così:
var a int
In questo caso, il valore verrà settato a 0 (e non a null: variabile senza puntatore di memoria). Usa la seguente sintassi per inizializzare ed assegnare un valore alla variabile nello stesso momento:
var a = 1
In questo caso, anche non avendo definito il tipo, verrà automaticamente inizializzata come un int.
È possibile anche utilizzare una versione short della dichiarazione, in questo modo:
message := "hello world"
Possiamo anche dichiarare più variabili nella stessa istruzione:
var b, c int = 2
Tipi di dato
Come altri linguaggi di programmazione, Go supporta diversi tipi di strutture dati. Vediamone alcuni:
Number, String e Boolean
Alcuni dei tipi di Number supportati sono int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr…
Il tipo string salva nella memoria una sequenza di bytes.
Il boolean invece utilizza la parola short bool.
Go supporta pure i numeri complessi con complex64 e complex128.
var a bool = true var b int = 1 var c string = 'hello world' var d float32 = 1.222 var x complex128 = cmplx.Sqrt(-5 + 12i)
Array, Slice e Map
Come già saprai un array è un insieme di elementi dello stesso tipo. Hanno una lunghezza fissa definita al momento della dichiarazione e non può essere espanso più di quello. È dichiarato in questo modo:
var a [5]int
Possono anche essere multidimensionali:
var multiD [2][3]int
Gli array sono limitanti in casi in cui i valori dell’array stesso cambiano a runtime. Inoltre non hanno funzioni native che ne permettano l’estrapolazione di un subarray. Per questo motivo Go ha realizzato una struttura chiamata Slice.
Gli slices storano una sequenza di elementi che può essere espansa quando si vuole. La dichiarazione è simile con la differenza che la capacità non è definita.
var b []int
Questa riga di codice, crea uno slice con 0 capienza e 0 elementi al suo interno. Gli slice possono essere definiti con capacità e lunghezza, come segue:
numbers := make([]int,5,10)
Lo slice ha una lunghezza iniziale di 5 ed una capacità di 10.
Essi sono una astrazione dell’array base. Utilizzano l’array come struttura sottostante. Uno slice contiene 3 componenti: capacità, lunghezza ed un puntatore all’array che si occupa dello store dei valori. Ecco un esempio grafico:

La capacità dello slice può essere aumentata utilizzando le funzioni copy ed append. Quest’ultima aggiunge valori alla fine dell’array ed inoltre ne aumenta la capacità se richiesto.
numbers = append(numbers, 1, 2, 3, 4)
Un altro modo per aumentare la capcaità di uno slice è utilizzando la funzione copy. Semplicemente creando un nuovo slice con una capacità più grande e questo metodo si occuperà del travaso dei contenuti.
// create a new slice number2 := make([]int, 15) // copy the original slice to new slice copy(number2, number)
È possibile creare un sub-slice da uno slice. Può essere fatto semplicemente con i seguenti comandi:
// initialize a slice with 4 len and values number2 = []int{1,2,3,4} fmt.Println(numbers) // -> [1 2 3 4] // create sub slices slice1 := number2[2:] fmt.Println(slice1) // -> [3 4] slice2 := number2[:3] fmt.Println(slice2) // -> [1 2 3] slice3 := number2[1:4] fmt.Println(slice3) // -> [2 3 4]
Le Map in Go sono una struttura di tipo chiave-valore. Possiamo definire un map utilizzando la seguente istruzione:
var m map[string]int
m è una nuova variabile di tipo map, che ha la chiave di tipo stringa ed il valore di tipo integer. Possiamo aggiungere semplicemente valori in questo modo:
// adding key/value m['clearity'] = 2 m['simplicity'] = 3 // printing the values fmt.Println(m['clearity']) // -> 2 fmt.Println(m['simplicity']) // -> 3
Casting
Un tipo di dato può essere convertito in un altro utilizzando la castizzazione (tipica di tutti i linguaggi). Vediamo un esempio:
a := 1.1 b := int(a) fmt.Println(b) //-> 1
Non tutti i tipi ovviamente si prestano alla conversione. Specialmente quelli realizzati ad-hoc.
Statement condizionali
if else
Per i condizionali, si possono utilizzare i classici if-else come mostrato nell’esempio in basso. Assicurati che le parentesi graffe siano posizionate in linea alla dichiarazione della condizione.
if num := 9; num < 0 { fmt.Println(num, "is negative") } else if num < 10 { fmt.Println(num, "has 1 digit") } else { fmt.Println(num, "has multiple digits") }
switch case
Gli switch aiutano ad organizzare condizioni multiple. Ecco un esempio:
i := 2 switch i { case 1: fmt.Println("one") case 2: fmt.Println("two") default: fmt.Println("none") }
Cicli
Go ha una sola parola chiave per i loop. Un solo comando permette di raggiungere diversi obiettivi:
i := 0 sum := 0 for i < 10 { sum += 1 i++ } fmt.Println(sum)
L’esempio in alto è simile al while di C. Lo stesso può essere utilizzato per un ciclo for semplice:
sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum)
Loop infiniti (while(true)) possono essere definiti in questo modo:
for { }
Puntatori
Go utilizza i puntatori. Essi sono definiti come l’indirizzo di memoria dei value associati alle variabili. Un puntatore è definito con *, associato al tipo di dato, al momento della dichiarazione della variabile.
var ap *int
ap è un puntatore ad un tipo int. L’operatore & può essere usato per ottenere l’indirizzo di una variabile.
a := 12 ap = &a
Il valore puntato dal puntatore può essere visualizzato utilizzando l’operatore *:
fmt.Println(*ap) // => 12
I puntatori sono preferiti quando bisogna passare una struct (oggetto) come argomento ad una funzione.
- Se non passassimo il puntatore il valore verrebbe copiato, quindi occuperebbe più memoria
- Passando solo il puntatore, il valore cambiato nella funzione verrebbe riflesso alla funzione chiamante, senza la necessità di restituire nuovi valori copia, e risparmiando spazio nella memoria.
Esempio:
func increment(i *int) { *i++ } func main() { i := 10 increment(&i) fmt.Println(i) } //=> 11
Funzioni
Come abbiamo detto all’inizio dell’articolo, la funzione principale definita nel package main, è l’entry point del tuo programma per essere eseguito. Possono essere definite altre funzioni ed utilizzate. Vediamo un esempio:
func add(a int, b int) int { c := a + b return c } func main() { fmt.Println(add(2, 1)) } //=> 3
Come possiamo vedere nell’esempio in alto, una funzione go è definita utilizzando la parola chiave func, seguita dal nome della funzione. Gli argomenti devono essere definiti in accordo col tipo che la funzione stessa si aspetterà di ricevere.
Anche il tipo di ritorno può essere definito nell’intestazione della funzione, come segue:
func add(a int, b int) (c int) { c = a + b return } func main() { fmt.Println(add(2, 1)) } //=> 3
c è definita come variabile di ritorno. E verrà restituita con la parola return alla fine del metodo. È possibile anche restituire più variabili separate da virgola.
func add(a int, b int) (int, string) { c := a + b return c, "successfully added" } func main() { sum, message := add(2, 1) fmt.Println(message) fmt.Println(sum) }
Metodi, Struct ed Interfacce
Go non è un linguaggio completamente Object-oriented, ma con le structs, le interfacce ed i metodi, fornisce un supporto completo per una logica ad oggetti.
Struct
Una struct è un tipo, un oggetto, una classe, una collezione di differenti attributi. Per esempio, se vogliamo raccogliere insieme tutti gli attributi di una persona, i quali includono nome, età e genere, potremo definire quanto segue:
type person struct { name string age int gender string }
Ora possiamo creare l’oggetto persona:
//way 1: specifying attribute and value p = person{name: "Bob", age: 42, gender: "Male"} //way 2: specifying only value person{"Bob", 42, "Male"}
Possiamo facilmente accedere ai dati con un punto (.).
p.name //=> Bob p.age //=> 42 p.gender //=> Male
Puoi inoltre accedere agli attributi di una struct direttamente con il suo puntatore:
pp = &person{name: "Bob", age: 42, gender: "Male"} pp.name //=> Bob
Metodi
I metodi sono un tipo speciale di funzione con un receiver. Un receiver può essere sia un valore che un puntatore. Andiamo ora a creare un metodo chiamato describe che ha un receiver di tipo persona:
package main import "fmt" // struct defination type person struct { name string age int gender string } // method defination func (p *person) describe() { fmt.Printf("%v is %v years old.", p.name, p.age) } func (p *person) setAge(age int) { p.age = age } func (p person) setName(name string) { p.name = name } func main() { pp := &person{name: "Bob", age: 42, gender: "Male"} pp.describe() // => Bob is 42 years old pp.setAge(45) fmt.Println(pp.age) //=> 45 pp.setName("Hari") fmt.Println(pp.name) //=> Bob }
Come possiamo vedere nell’esempio in alto, il metodo può essere richiamato utilizzando il punto dopo la variabile dell’oggetto: pp.describe.
Considera che in questo caso il receiver è un puntatore. Con il puntatore passiamo il riferimento al valore, quindi se facciamo qualsiasi tipo di cambiamento al metodo, esso si rifletterà sul receiver pp. Inoltre non verrà creata una copia dell’oggetto, cosa che ci farà risparmiare memoria.
Come hai potuto vedere il valore di age è cambiato, mentre il valore di name non è cambiato perché il metodo setName non ha riferimento al puntatore nel receiver, mentre setAge si.
Interfacce
Le interfacce in Go sono collezioni di metodi. Aiutano a raggruppare le varie proprietà o metodi ausiliari correlati ad un tipo. Guarda questo esempio di interfaccia:
type animal interface { description() string }
animal è un’interfaccia. Ora andiamo a creare due differenti tipi di animali che implementano questa interfaccia:
package main import ( "fmt" ) type animal interface { description() string } type cat struct { Type string Sound string } type snake struct { Type string Poisonous bool } func (s snake) description() string { return fmt.Sprintf("Poisonous: %v", s.Poisonous) } func (c cat) description() string { return fmt.Sprintf("Sound: %v", c.Sound) } func main() { var a animal a = snake{Poisonous: true} fmt.Println(a.description()) a = cat{Sound: "Meow!!!"} fmt.Println(a.description()) } //=> Poisonous: true //=> Sound: Meow!!!
Nella funzione main, abbiamo creato la variabile a di tipo animal. Assegnamo i tipi snake e cat agli animali ed usiamo PrintLn per stamparne la descrizione. Siccome abbiamo implementato il metodo descritto in alto in entrambi i tipi, riusciamo a raggiungerlo senza la necessità di scriverlo due volte.
Packages
Tutto il codice che scriviamo in Go va a finire in dei package. Il package main è l’entri point per l’esecuzione del programma. Ci sono molti pacchetti che possiamo utilizzare, il più famoso è fmt.
Installare un package
go get <package-url-github> // example go get github.com/satori/go.uuid
I pacchetti che installiamo vanno a finire nella GOPATH, che rappresenta la nostra directory di lavoro. Puoi vedere il package andando dentro la cartella pkg: cd $GOPATH/pkg.
Creiamo un package personalizzato
Iniziamo con creare una cartella chiamata custom_package:
> mkdir custom_package > cd custom_package
Ipotizziamo di creare un package person. Eseguiamo quindi i seguenti comandi:
> mkdir person > cd person
Ora creiamo un file person.go dentro questa cartella con il seguente contenuto:
package person func Description(name string) string { return "The person name is: " + name } func secretName(name string) string { return "Do not share" }
Ora installiamo il package in modo tale che possa essere utilizzato nel nostro codice:
> go install
Ora nella cartella custom_package creiamo il file main.go:
package main import( "custom_package/person" "fmt" ) func main(){ p := person.Description("Milap") fmt.Println(p) } // => The person name is: Milap
Qui possiamo andare ad importare il package person che abbiamo creato ed usare la funzione Description. Nota che la funzione secretName che abbiamo creato nel package non sarà accessibile. In Go, i nomi di funzioni ed attributi che non iniziano con la prima lettera maiuscola, vengono considerati privati.
Documentazione dei package
Go offre un supporto nativo per la realizzazione di documentazione. Eseguendo il seguente comando verrà generata automaticamente la documentazione:
godoc person Description
Questo comando genererà la documentazione per la funzione Description all’interno del nostro package. Per vedere la documentazione lanciare il comando:
godoc -http=":8080"
Verrà lanciato un web server sulla porta 8080 all’indirizzo http://localhost:8080/pkg/.
Altri pacchetti nativi
fmt
Questo pacchetto, come hai potuto vedere precedentemente, implementa le funzioni di Input/Output.
json
Un altro package utile si chiama json. Aiuta a codificare/decodificare il principale linguaggio di comunicazione per le REST api:
Encode
package main import ( "fmt" "encoding/json" ) func main(){ mapA := map[string]int{"apple": 5, "lettuce": 7} mapB, _ := json.Marshal(mapA) fmt.Println(string(mapB)) }
Decode
package main import ( "fmt" "encoding/json" ) type response struct { PageNumber int `json:"page"` Fruits []string `json:"fruits"` } func main(){ str := `{"page": 1, "fruits": ["apple", "peach"]}` res := response{} json.Unmarshal([]byte(str), &res) fmt.Println(res.PageNumber) } //=> 1
La funzione Unmarshal ha come primo argomento il byte della stringa json, mentre come secondo argomento, l’indirizzo di memoria della variabile che contiene l’oggetto da mappare. json:”page” è utilizzato per la paginazione (Guarda la documentazione per approfondire questo argomento).
Gestione degli errori
Gli errori sono la cosa meno desiderata dai programmatori, ma anche la più inevitabile da ottenere. Immaginiamo di dover implementare una chiamata API ad un servizio esterno. Questa chiamata può andare a buon fine come fallire. Un errore in Go può essere riconosciuto nel momento in cui viene valorizzato l’oggetto err:
resp, err := http.Get("http://example.com/")
Con il seguente codice possiamo controllare se la chiamata fallisce o va a buon fine:
package main import ( "fmt" "net/http" ) func main(){ resp, err := http.Get("http://example.com/") if err != nil { fmt.Println(err) return } fmt.Println(resp) }
Come specializzare l’errore
Quando scriviamo una funzione per conto nostro, ci sono casi in cui potrebbero esserci degli errori, che non dipendono dalla scrittura del nostro codice. La maggior parte delle volte dovute all’utilizzo di servizi esterni. In queste occasioni, possiamo utilizzare sempre l’oggetto error:
func Increment(n int) (int, error) { if n < 0 { // return error object return nil, errors.New("math: cannot process negative number") } return (n + 1), nil } func main() { num := 5 if inc, err := Increment(num); err != nil { fmt.Printf("Failed Number: %v, error message: %v", num, err) }else { fmt.Printf("Incremented Number: %v", inc) } }
La maggior parte dei package realizzati in Go, o altri package esterni, hanno un meccanismo di gestione degli errori. Quindi ogni funzione che andremo ad utilizzare, dovrebbe avere possibili errori. Questi errori non dovrebbero essere mai ignorati e gestiti sempre in modo giusto.
Panic
Il così detto “Panic” è quella fase del runtime di una applicazione che non è gestibile e non dipende né da noi, né da eventuali chiamate esterne. In Go, il panic non è il modo migliore per gestire le eccezioni. È raccomandato sempre l’utilizzo di un oggetto di tipo errore. Quando avviene un panic, il runtime viene improvvisamente fermato, e subito dopo eseguita la funzione defer.
Defer
Il Defer è qualcosa che viene sempre eseguito alla fine di una funzione.
//Go package main import "fmt" func main() { f() fmt.Println("Returned normally from f.") } func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }
Nell’esempio in alto, “mandiamo in panico” l’esecuzione di una funzione utilizzando panic(). Come hai potuto notare, c’è un defer che alla fine che lancia un’istruzione di print. Può anche però essere utilizzato in situazioni meno estreme, come quella di chiudere un file alla fine dell’esecuzione di una funzione.
Concorrenza
Go è stato realizzato avendo sempre in mente la così detta Concurrency. Essa può essere raggiunta le Go routine, che sono dei thread leggeri che vengono lanciati simultaneamente al thread principale.
Go routine
Le routine sono delle funzioni che possono essere eseguite parallelamente al thread principale. Sono molto semplici da realizzare: basta inserire la parole go prima della funzione ed automaticamente questa verrà eseguita in parallelo. Le routine di Go sono molto leggere, pertanto non hanno un elevato peso sulla memoria. È possibile crearne migliaia. Vediamo un esempio:
package main import ( "fmt" "time" ) func main() { go c() fmt.Println("I am main") time.Sleep(time.Second * 2) } func c() { time.Sleep(time.Second * 2) fmt.Println("I am concurrent") } //=> I am main //=> I am concurrent
Come puoi vedere nell’esempio, la funzione c è una routine che esegue istruzioni in parallelo al thread principale.
Tuttavia, ci sono momenti in cui abbiamo necessità di condividere informazioni fra thread diversi. Go preferisce non permettere la condivisione di variabili perché potrebbero causare deadlock. C’è però un altro modo per condividere le risorse fra thread diversi: i go channels.
Channels
Possiamo passare dati fra due routine utilizzando i channels. Per creare un channel è necessario specificare che tipo di dati il canale in questione sta per ricevere. Creiamo un channel con questo esempio:
c := make(chan string)
Con questo channel, possiamo passare un dato di tipo stringa. Possiamo sia inviare che ricevere informazioni:
package main import "fmt" func main(){ c := make(chan string) go func(){ c <- "hello" }() msg := <-c fmt.Println(msg) } //=>"hello"
Il channel ricevente attende che il mittente invii i dati per poterli elaborare.
One way channel
Ci sono casi in cui vogliamo che una routine Go non abbia la possibilità di comunicare con il thread principale nel senso opposto. Ma che sia solo pronta a ricevere ed elaborare i nostri dati. Per questo, possiamo creare un one-way-channel. Vediamo il prossimo esempio:
package main import ( "fmt" ) func main() { ch := make(chan string) go sc(ch) fmt.Println(<-ch) } func sc(ch chan<- string) { ch <- "hello" }
Nell’esempio in alto, sc è una routine che può solo inviare messaggi al canale ma non può riceverne.
Organizzare più canali per una routine utilizzando select
Ci potrebbero essere routine che attendono dati da più canali. Per questo possiamo usare i select. Per fare più chiarezza, vediamo il seguente esempio:
package main import ( "fmt" "time" ) func main() { c1 := make(chan string) c2 := make(chan string) go speed1(c1) go speed2(c2) fmt.Println("The first to arrive is:") select { case s1 := <-c1: fmt.Println(s1) case s2 := <-c2: fmt.Println(s2) } } func speed1(ch chan string) { time.Sleep(2 * time.Second) ch <- "speed 1" } func speed2(ch chan string) { time.Sleep(1 * time.Second) ch <- "speed 2" }
Il main aspetta su due canali, c1 e c2. Con il select (equivalente di switch, ma per le routine), possiamo eseguire istruzioni diverse in base da quale sia il canale dal quale viene quel determinato tipo di dato.
Buffered channel
Ci sono asi in cui abbiamo bisogno di inviare dati multipi ad un canale. Per fare ciò, puoi creare un buffer. In questo modo, il receiver non otterrà il tuo messaggio finché il buffer non sarà stato completato:
package main import "fmt" func main(){ ch := make(chan string, 2) ch <- "hello" ch <- "world" fmt.Println(<-ch) }
Perché Golang ha successo?
Semplicità.
Ottimo! 😀
Abbiamo imparato alcuni dei maggiori componenti che costituiscono il linguaggio Go.
- Variabili, Tipi di dato
- Array slices e maps
- Funzioni
- Loop e condizionali
- Puntatori
- Packages
- Metodi, Strutture ed interfacce
- Gestione degli errori
- Concurrecy – Go routines e channels
Congratulazioni, ora hai una conoscenza iniziale di Go e puoi cominciare ad utilizzarlo.
Non fermarti qui. Pensa ad una piccola applicazione che vorresti realizzare ed inizia a costruirla! Puoi farcela.
Se il mio tutorial ti è piaciuto, condividilo sui social. Te ne sarò molto grato! 😀
Alla prossima!