Guida introduttiva al linguaggio di programmazione Go

Guida introduttiva al linguaggio di programmazione Go

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.

Nuovo logo di Go

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:

Immagine da: https://blog.golang.org/go-slices-usage-and-internals

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.

  1. Se non passassimo il puntatore il valore verrebbe copiato, quindi occuperebbe più memoria
  2. 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.

  1. Variabili, Tipi di dato
  2. Array slices e maps
  3. Funzioni
  4. Loop e condizionali
  5. Puntatori
  6. Packages
  7. Metodi, Strutture ed interfacce
  8. Gestione degli errori
  9. 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!

Leave a Comment

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *