Go - from Google



Dati          

Per poter lavorare ho sempre apprezzato avere la conoscenza dei dati su cui operare. Mi sembra un primo passo logico per imparare un linguaggio e, possibilmente, seguo questo passaggio di apprendimento per ogni linguaggio con il quale mi voglio cimentare.
Nel precedente paragrafo abbiamo visto come definire gli identificatore, dal punto di vista sintattico. Prima di continuare è importante sapere, per definire le variabili,  che in Go si usano le seguenti keyword:

var - per le variabili il cui valore può essere modificato nel corso del programma
const - per le costanti, ovvero quegli elementi (chiamarli variabili ora sarebbe un po' un controsenso) il cui valore resterà immutabile.
assegnazione diretta tramite l'operatore := (vedi di seguito)

A livello di dichiarazione, ricordiamoci che Go supporta l'inferenza di tipo, ovvero la capacità da parte del compilatore, di dedurre il tipo di una variabile o di una costante basandosi sul valore ad esso attribuito in sede di dichiarazione. E' una caratteristica quasi immancabile in molti linguaggi di nuova generazione. Quindi:

var x := 8
var y int
y = 8


sono due modi per attribuire il valore 8 ad una variabile. Nel secondo caso abbiamo anche imposto che sia di tipo int (cosa sia lo vedremo tra breve) nel primo il compilatore lo ricava da se, Se vogliamo ottenere il tipo di ogni variabile o costante dobbiamo usare il package reflect:

Esempio 2.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"reflect"
)

func main() {
x := 9
var y int
y = 9
fmt.Println(reflect.TypeOf(x))
fmt.Println(reflect.TypeOf(y))
}

Numeri

Questa tipologia di dato è definibile tramite la seguente tabella, ricordando che più avanti, in questo stesso paragrafo, troveremo una definizione puramente formale:

Tipo Range
  Interi  
  Int8 -128 : 127
  Int16 -32768 : 32767
  Int32 -2147483648 : 2147483647
  Int64 -9223372036854775808 : 9223372036854775807
  Uint (interi senza segno)  
  Uint8 0 : 255
  Uint16 0 : 65535
  Uint32 0 : 4294967295
  Uint6 0 : 18446744073709551615
  Float (numeri con virgola)  
  Float32 +-10^-45 : +-3.4 * 10^38
  Float64 +-5 + 10^-324 : 1.7 * 10^308
  Uintptr (puntatore)  
  Uintptr contiene un puntatore

Esistono un paio di alias: byte che sta per unit8 e rune per invece sta per int32. Vedremo inoltre i numeri complessi.
Il valore di default è ovviamente 0 per gli interi e 0.0 per i Float.

Le operazioni di base sono le solite, vediamo quelle di base con un esempio:

  Esempio 2.2
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main(){
var x rune
x = 10
var y rune
y = 3
fmt.Println(x + y)  // somma
fmt.Println(x - y)  // sottrazione
fmt.Println(x * y)  // moltiplicazione
fmt.Println(x / y)  // divisione
fmt.Println(x % y)  // resto della divisione
}


I float 32 hanno precisiome fino alla settima cifra decimale, i float64, il cui uso è raccomandato laddove possibile, fino alla 15esima, come più o meno consueto in altri linguaggi, nzi al momento non mi sovvengono eccezioni. Il package matematico di Go si aspetta di lavorare, come default, con i float64.
Esistono altri operatori, presenti in molti altri linguaggi, che possono venire utili nella pratica:

Operatore Esempio Esito
++ x++ Incrementa x della costante non tipizzata 1
-- x-- Decremente x della costante non tipizzata 1
+= x+=y Somma x a y
-= x-=y decrementa x di y
*= x*=y Moltiplica x per y
/= x/=y Dividie x per y, risultando un valore intero, il resto viene scartato

E sono disponibili anche i consueti operatori per le operazioni a livello di bit, ne fornisco solo un accenno, sono in tutto e per tutto simili a quanto si trova nella letteratura matematica disponibile:

^  per realizzare il complemento bit a bit ( ^x )
&  per l'AND tra due elementi (x & y)
|   per l'OR tra due elementi ( x | y )
^  per il NOT tra due elementi ( x ^ y) ovviamente è un uso diverso rispetto al primo caso
<< shift a sinistra di u (unsigned) posizioni
>> shift a destra di u (unsigned) posizioni

esempio:

x := 1
y := 3
fmt.Println(^x)
fmt.Println(x & y)


Anche in Go è possibile usare la notazione ottale facendo precedere il numero da un "0" oppure in esadecimale con il classico prefisso "0x". Ovvero

077 -> ottale
0xE -> esadecimale


oppure possiamo adottare la notazione esponenziale:

2e4 = 20000
1.234e5 = 1.234 x 10^5 = 123400.


In questo non vi è nulla di diverso da quanto possiamo trovare altrove..
Go è un linguaggio fortemente tipizzato per cui non è permesso mischiare tipi diversi. Quindi il seguente codice non compila:

var x int
var y rune
x = 5
y = 6
y = y + x // errore


mentre va bene scrivere ad esempio:

y = y + 3

in quanto 3 è un valore costante e non una variabile. L'errore illustrato qui sopra ci fa capire come Go non fa sconti e non permette conversioni implicite in questo ambito, il che, a mio avviso, è cosa molto buona, sono assolutamente contrario ai compilatori che fanno le cose "sottobosco" senza evidenziare nulla del loro operato.
Detto questo potreste avere la curiosità di vedere come si può realizzare il cast che può essere utile anche per vedere come trovare il risultato esatto di una divisione come quella presente nell'esempio 2.1. Lesempio seguente realizza quanto detto:

  Esempio 2.3
1
2
3
4
5
6
7
8
9
10
11
package main
import "fmt"
func main(){
var x rune
x = 10
var y int
y = 3
var z float64
z = float64(x) / float64(y)
fmt.Println(z)
}


La riga 9, evidenziata in rosso, propone il cast dei due interi a float64. La divisone permette di ottenere il risultato corretto a livello di decimali (fino al 15esimo). Provate a vedere cosa accade mettendo float invece di float64. A questo proposito si faccia attenzione al confronto tra due elementi float che non dovrebbe in genere essere impostata in termini di "uguaglianza" assoluta ma come differenza minore di un certo valore piccolo a piacere. Insomma, dati due valori float f1 ed f2 si potrà dire che sono uguali se |f1 - f2| < ε con ε che è il nostro valore di soglia.
Nell'esempio, il risultato della divisione presente alla riga 9 è quello "vero". Il cast viene realizzato evidentemente utilizzando il classico formato:

tipo(dato)

La rappresentazione in output dei numeri può essere proposta in vari modi:

%d    -- per gli interi
%x    -- per gli esadecimali
%X    -- ancora per gli esadecimali
%f    -- per i floating point 32
%g    -- per i floating point 64
%0nd  -- indica gli interi espressi su n cifre eventualmente premettendo degli 0
%n.mg -- esprime i float con n spazi nella parte intera seguite da m spazi in quella decimale

Vediamo un esempio:

  Esempio 2.4
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
func main(){
var x float64
x = 2.23
var y rune
y = 100
fmt.Printf("numero %d\n", y)
fmt.Printf("numero %x\n", y)
fmt.Printf("numero %f\n", x)
fmt.Printf("numero %e\n", x)
fmt.Printf("numero %10.4g\n", x)
}

Per completare, almeno in prima istanza, il quadro relativo ai numeri non si può fare a meno di segnalare il potentissimo package math che contiene numerose funzioni avanzate, trigonometriche, logaritmiche, la definizione delle più comuni costanti matematiche ecc... Di seguito fornisco un esempio ovviamente solo indicativo, ricordando che math lavora bene con i float64 e che le funzioni per essere utilizzabili all'esterno del proprio package di appartenenza devono iniziare con la maiuscola, come detto in precedenza:

  Esempio 2.5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
import "math"
func main(){
var y float64
y = 100.00
fmt.Printf("Elevamento a potenza: %f\n", math.Pow(y, 2))
fmt.Printf("Elevamento a potenza: %f\n", math.Pow(y, 0.5))
fmt.Printf("Valore assoluto %f\n", math.Abs(-50))
fmt.Printf("Radice quadrata di 2 = %g\n", math.Sqrt(2))
fmt.Printf("La costante e: %g\n", math.E)
fmt.Printf("Pi greco: %g\n", math.Pi)
fmt.Printf("Massimo intero: %d\n", math.MaxInt32)
}

Booleani

Lasciamo i numeri e passiamo ai valori booleani. Si tratta della tipologia più semplice che ammette solo due valore: true e false, anche qui direi "come di consueto". La keyword che identifica questo tipo è bool. Il valore di default è false.

var x bool
x = true


Esempio:

  Esempio 2.6
1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
func main(){
var bool1 bool
if bool1 {
fmt.Println("true")
} else {
fmt.Println("false")
}
}


Questo tipo di dato non ha altre particolarità.

Caratteri e composti

Con questo paragrafo intendiamo comprendere i cosiddetti "literals" che in realtà comprendono sia un singolo carattere sia una sequenza di caratteri racchiusa in una coppia di singoli apici. Un'ampia gamma di cose insomma. Andiamo avanti in maniera schematica, seguendo quanto proposto sul sito ufficiale.

Cos'è un carattere? Semplice:

newline = /*il classico carattere U+000A */ .
unicode_char = /* un carattere arbitrario escludendo il newline */ .
unicode_letter = /* un carattere unicode classificabile come "lettera"" */ .
unicode_digit = /* un carattere unicode classificabile come "numero" */ .


Vediamo ora in dettaglio le lettere e i numeri:

lettere = lettere unicode | "_" .
decimali = "0" … "9" .
ottali = "0" … "7" .
esadecimali = "0" … "9" | "A" … "F" | "a" … "f"


Interi:

interi = decimali | ottali | esadecimali
decimali = ( "1" … "9" ) { decimali } .
octal_lit = "0" { ottali } .
hex_lit = "0" ( "x" | "X" ) esadecimali { esadecimali } .

Quindi niente di nuovo rispetto a quanto già visto ma solo una semplice formalizzazione.

Floating point

floating = decimali "." [ decimali ] [ esponente ] |
esponente decimale |
"." decimali [ esponente ] .
decimali = cifra decimale { cifra decimale } .
esponente = ( "e" | "E" ) [ "+" | "-" ] decimali .

anche qui nulla di nuovo rispetto a quanto noto in altri linguaggi, il formalismo non è in realtà complicato. Vediamo qualche esempio:

1.
2.345
1.e+1
2E7
.12345e+6

Caratteri letterali

NB questa sezione è un abbozzo che tratta delle stringhe creato per poter iniziare a lavorare consapevolmente su queste: relativamente a questo argomento esiste, per gli approfondimenti del caso, un capitolo dedicato.

Con questa definizione intendiamo un singolo carattere posto all'interno di una coppia di apici. In realtà il tipo "carattere" non esiste, si tratta semplicemente di un sottoinsieme, un caso speciale se vogliamo, del tipo intero. Di seguito un esempio veloce:

  Esempio 2.7
1
2
3
4
5
6
package main
import "fmt"
func main(){
var ch byte = 65
fmt.Printf("%c",ch)
}

Questo esempio stampa la lettera 'A' a video.

In realtà Go prevede due tipi di letterali: raw e interpretate.
I primi sono in effetti delle sequenza di caratteri posti all'interno ad una coppia di singoli apici gli altri, che possiamo identificare come "stringhe" vere e proprie avvicinandoci agli altri linguaggi di programmazione in termini di nomenclatura, sono comprese tra una coppia di doppi apici.
All'interno di una stringa raw è legale ogni carattere, a parte il "back". Il valore della stringa è il risultato della concatenazione dei singoli caratteri componenti che non sono soggetti ad alcuna interpretazione e possono quindi contenere backslash, CR+LF ecc... Il fatto che parliamo della assenza di interpretazione significa semplicemente che, ad esempio, le sequenze di escape non vengono prese in considerazione se non il loro valore di caratteri presi in sequenza, ovvero \t è come esso appare e non viene interpretato come una tabulazione. Nelle stringhe interpretate, come detto quello racchiuse all'interno di una coppia di doppi apici, invece tale sequenza viene interpretata come una tabulazione mentre, per fare un altro esempio, \n equivale ad un "a capo" nelle stringhe non raw.
In questo linguaggio tutte le stringhe sono delimitate dalla propria lunghezza, ovvero non ci sono caratteri terminatori particolari come invece ad esempio in C++. ed il suo valore di default è la stringa vuota (""). Gli elementi di una stringa sono indicizzati a partire a 0 (zero based) e progressivamente fino all'indice pari al numero degli elementi -1- L'accesso al singolo elemento avviene tramite l'operatore [].
Con le stringhe, come in tutti i linguaggi che si rispettino, abbiamo la possibilità di fare tante belle operazioni. Vediamo qualcosa tanto per avere degli strumenti con cui lavorare in seguito:

  Esempio 2.8
1
2
3
4
5
6
7
8
9
10
11
11
package main
import "fmt"
func main(){
var s1 = "abcd"
var s2 = "efgh"
fmt.Println(s1 + s2)
fmt.Println(s1 == s2)
fmt.Println(s1 != s2)
fmt.Println(s1 > s2)
fmt.Println(s1 < s2)
fmt.Println(len(s1))
}

Detto che sugli operatori torneremo in seguito tutto dovrebbe essere abbastanza intuitivo. Alla riga 6 l'operatore + permette la concatenazione di due o più stringhe. Dalla 7 alla 10 abbiamo in azione gli operatori di confronto (sono ammessi anche <= e >=) mentre la 11 espone la lunghezza, ovvero il numero di elementi. Come detto non c'è nulla di nuovo. Ma naturalmente non finisce qui. Esiste un package specifico, che si chiama guarda caso "string", che contiene parecchie funzioni specifiche pronte all'uso. Di seguito l'elenco completo, i nomi delle funzioni sono "parlanti" insieme ad un paio di esempi tanto per chiarire l'uso. Sul sito ufficiale comunque potrete trovare tutti gli approfondimenti del caso. L'ultimo tipo esposto nella tabella seguente è quello di ritorno della funzione.

func Contains(s, substr string) bool
func ContainsAny(s, chars string) bool
func ContainsRune(s string, r rune) bool
func Count(s, sep string) int
func EqualFold(s, t string) bool
func Fields(s string) []string
func FieldsFunc(s string, f func(rune) bool) []string
func HasPrefix(s, prefix string) bool
func HasSuffix(s, suffix string) bool
func Index(s, sep string) int
func IndexAny(s, chars string) int
func IndexFunc(s string, f func(rune) bool) int
func IndexRune(s string, r rune) int
func Join(a []string, sep string) string
func LastIndex(s, sep string) int
func LastIndexAny(s, chars string) int
func LastIndexFunc(s string, f func(rune) bool) int
func Map(mapping func(rune) rune, s string) string
func Repeat(s string, count int) string
func Replace(s, old, new string, n int) string
func Split(s, sep string) []string
func SplitAfter(s, sep string) []string
func SplitAfterN(s, sep string, n int) []string
func SplitN(s, sep string, n int) []string
func Title(s string) string
func ToLower(s string) string
func ToLowerSpecial(_case unicode.SpecialCase, s string) string
func ToTitle(s string) string
func ToTitleSpecial(_case unicode.SpecialCase, s string) string
func ToUpper(s string) string
func ToUpperSpecial(_case unicode.SpecialCase, s string) string
func Trim(s string, cutset string) string
func TrimFunc(s string, f func(rune) bool) string
func TrimLeft(s string, cutset string) string
func TrimLeftFunc(s string, f func(rune) bool) string
func TrimRight(s string, cutset string) string
func TrimRightFunc(s string, f func(rune) bool) string
func TrimSpace(s string) string


  Esempio 2.9
1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
import "strings"
func main(){
var s1 = "aabbccdd"
fmt.Println(strings.Contains(s1, "ab"))
fmt.Println(strings.Index(s1, "ab"))
fmt.Println(strings.ToUpper(s1))
fmt.Println(s1[3]) // attenzione all'output
}

Ovviamente torneremo ancora su questi argomenti.