Comprendere i Tipi di Dati in Go

Introduzione

I tipi di dati specificano i tipi di valori che particolari variabili memorizzeranno quando si scrive un programma. Il tipo di dati determina anche quali operazioni possono essere eseguite sui dati.

In questo articolo, tratteremo i tipi di dati importanti nativi di Go. Questa non è un’indagine esaustiva sui tipi di dati, ma ti aiuterà a familiarizzare con le opzioni disponibili in Go. Comprendere alcuni tipi di dati di base ti permetterà di scrivere codice più chiaro ed efficiente.

Background

Un modo per pensare ai tipi di dati è considerare i diversi tipi di dati che utilizziamo nel mondo reale. Un esempio di dati nel mondo reale sono i numeri: potremmo usare numeri interi (0, 1, 2, …), numeri interi (…, -1, 0, 1, …), e numeri irrazionali (π), ad esempio.

Di solito, in matematica, possiamo combinare numeri di diversi tipi e ottenere una sorta di risposta. Potremmo voler aggiungere 5 a π, ad esempio:

5 + π

Possiamo mantenere l’equazione come risposta per tener conto del numero irrazionale, o arrotondare π a un numero con un numero ridotto di decimali, e poi sommare i numeri:

5 + π = 5 + 3.14 = 8.14 

Ma, se iniziamo a cercare di valutare numeri con un altro tipo di dato, come le parole, le cose iniziano a perdere di senso. Come risolveremmo l’equazione seguente?

shark + 8

Per i computer, ogni tipo di dato è abbastanza diverso—come parole e numeri. Di conseguenza, dobbiamo fare attenzione a come utilizziamo vari tipi di dati per assegnare valori e come li manipoliamo attraverso operazioni.

Interi

Come in matematica, interi nella programmazione dei computer sono numeri interi che possono essere positivi, negativi o 0 (…, -1, 0, 1, …). In Go, un intero è noto come int. Come con altri linguaggi di programmazione, non si dovrebbero usare virgole in numeri di quattro cifre o più, quindi quando scrivi 1.000 nel tuo programma, scrivilo come 1000.

Possiamo stampare un intero in modo semplice in questo modo:

fmt.Println(-459)
Output
-459

Oppure, possiamo dichiarare una variabile, che in questo caso è un simbolo del numero che stiamo utilizzando o manipolando, così:

var absoluteZero int = -459
fmt.Println(absoluteZero)
Output
-459

Possiamo fare matematica con gli interi in Go, anche. Nel seguente blocco di codice, useremo l’operatore di assegnazione := per dichiarare e istanziare la variabile sum:

sum := 116 - 68
fmt.Println(sum)
Output
48

Come mostra l’output, l’operatore matematico - ha sottratto l’intero 68 da 116, risultando in 48. Imparerai di più sulla dichiarazione delle variabili nella sezione Dichiarazione dei Tipi di Dati per le Variabili.

Gli interi possono essere utilizzati in molti modi all’interno dei programmi Go. Continuando a imparare Go, avrai molte opportunità di lavorare con gli interi e costruire sul tuo conoscenza di questo tipo di dato.

Numeri in virgola mobile

Un numero in virgola mobile o un float è utilizzato per rappresentare numeri reali che non possono essere espressi come interi. I numeri reali includono tutti i numeri razionali e irrazionali, e per questo motivo, i numeri in virgola mobile possono contenere una parte frazionaria, come 9.0 o -116.42. Ai fini di pensare a un float in un programma Go, è un numero che contiene un punto decimale.

Come abbiamo fatto con gli interi, possiamo stampare un numero in virgola mobile in modo semplice in questo modo:

fmt.Println(-459.67)
Output
-459.67

Possiamo anche dichiarare una variabile che rappresenta un float, così:

absoluteZero := -459.67
fmt.Println(absoluteZero)
Output
-459.67

Proprio come con gli interi, possiamo fare matematica con i float in Go, anche:

var sum = 564.0 + 365.24
fmt.Println(sum)
Output
929.24

Con gli interi e i numeri in virgola mobile, è importante tenere a mente che 3 ≠ 3.0, poiché 3 si riferisce a un intero mentre 3.0 si riferisce a un float.

Dimensioni dei tipi numerici

Oltre alla distinzione tra numeri interi e numeri in virgola mobile, Go ha due tipi di dati numerici che si distinguono per la natura statica o dinamica delle loro dimensioni. Il primo tipo è un tipo indipendente dall’architettura, il che significa che la dimensione dei dati in bit non cambia, indipendentemente dalla macchina su cui il codice è in esecuzione.

La maggior parte delle architetture dei sistemi oggi sono o a 32 bit o a 64 bit. Ad esempio, potresti sviluppare per un moderno laptop Windows, su cui il sistema operativo funziona su un’architettura a 64 bit. Tuttavia, se stai sviluppando per un dispositivo come un orologio fitness, potresti lavorare con un’architettura a 32 bit. Se utilizzi un tipo indipendente dall’architettura come int32, indipendentemente dall’architettura per cui compili, il tipo avrà una dimensione costante.

Il secondo tipo è un tipo specifico dell’implementazione. In questo tipo, la dimensione in bit può variare in base all’architettura su cui il programma è costruito. Ad esempio, se usiamo il tipo int, quando Go compila per un’architettura a 32 bit, la dimensione del tipo di dato sarà di 32 bit. Se il programma è compilato per un’architettura a 64 bit, la variabile sarà di 64 bit.

Oltre a tipi di dati con dimensioni diverse, tipi come gli interi sono disponibili anche in due tipi di base: con segno e senza segno. Un int8 è un intero con segno e può avere un valore compreso tra -128 e 127. Un uint8 è un intero senza segno e può avere solo un valore positivo compreso tra 0 e 255.

Gli intervalli sono basati sulla dimensione in bit. Per i dati binari, 8 bit possono rappresentare un totale di 256 valori diversi. Poiché un tipo int deve supportare sia valori positivi che negativi, un intero a 8 bit (int8) avrà un intervallo da -128 a 127, per un totale di 256 valori unici possibili.

Go ha i seguenti tipi interi indipendenti dall’architettura:

uint8       unsigned  8-bit integers (0 to 255)
uint16      unsigned 16-bit integers (0 to 65535)
uint32      unsigned 32-bit integers (0 to 4294967295)
uint64      unsigned 64-bit integers (0 to 18446744073709551615)
int8        signed  8-bit integers (-128 to 127)
int16       signed 16-bit integers (-32768 to 32767)
int32       signed 32-bit integers (-2147483648 to 2147483647)
int64       signed 64-bit integers (-9223372036854775808 to 9223372036854775807)

Anche i numeri float e i numeri complessi vengono in diverse dimensioni:

float32     IEEE-754 32-bit floating-point numbers
float64     IEEE-754 64-bit floating-point numbers
complex64   complex numbers with float32 real and imaginary parts
complex128  complex numbers with float64 real and imaginary parts

Ci sono anche un paio di tipi numerici alias, che assegnano nomi utili a specifici tipi di dati:

byte        alias for uint8
rune        alias for int32

Lo scopo dell’alias byte è rendere chiaro quando il tuo programma utilizza byte come misura comune nel calcolo nei caratteri delle stringhe, al contrario di piccoli numeri interi non correlati alla misura dei dati dei byte. Anche se byte e uint8 sono identici una volta compilato il programma, byte viene spesso utilizzato per rappresentare dati carattere in forma numerica, mentre uint8 è destinato a essere un numero nel tuo programma.

L’alias rune è un po’ diverso. Dove byte e uint8 sono esattamente gli stessi dati, una rune può essere un singolo byte o quattro byte, un intervallo determinato da int32. Una rune viene utilizzata per rappresentare un carattere Unicode, mentre solo caratteri ASCII possono essere rappresentati esclusivamente dal tipo di dati int32.

Inoltre, Go ha i seguenti tipi specifici dell’implementazione:

uint     unsigned, either 32 or 64 bits
int      signed, either 32 or 64 bits
uintptr  unsigned integer large enough to store the uninterpreted bits of a pointer value 

I tipi specifici dell’implementazione avranno la loro dimensione definita dall’architettura per cui il programma viene compilato.

Scelta dei Tipi di Dati Numerici

La scelta della dimensione corretta ha generalmente più a che fare con le prestazioni per l’architettura di destinazione per cui stai programmando che con la dimensione dei dati con cui stai lavorando. Tuttavia, senza bisogno di conoscere le specifiche implicazioni in termini di prestazioni per il tuo programma, puoi seguire alcune di queste linee guida di base quando inizi per la prima volta.

Come discusso in precedenza in questo articolo, ci sono tipi indipendenti dall’architettura e tipi specifici dell’implementazione. Per i dati interi, è comune in Go utilizzare i tipi di implementazione come int o uint invece di int64 o uint64. Questo generalmente risulterà nella velocità di elaborazione più rapida per la tua architettura di destinazione. Ad esempio, se utilizzi un int64 e compili per un’architettura a 32 bit, ci vorrà almeno il doppio del tempo per elaborare quei valori poiché richiede cicli di CPU aggiuntivi per spostare i dati attraverso l’architettura. Se invece utilizzassi un int, il programma lo definirebbe come una dimensione a 32 bit per un’architettura a 32 bit e sarebbe significativamente più veloce da elaborare.

Se sai che non supererai un determinato intervallo di dimensioni, allora scegliere un tipo indipendente dall’architettura può aumentare la velocità e diminuire l’uso della memoria. Ad esempio, se sai che i tuoi dati non supereranno il valore di 100 e saranno solo numeri positivi, allora scegliere un uint8 renderebbe il tuo programma più efficiente in quanto richiederebbe meno memoria.

Ora che abbiamo esaminato alcuni dei possibili intervalli per i tipi di dati numerici, vediamo cosa succederà se superiamo questi intervalli nel nostro programma.

Overflow vs. Wraparound

Go ha la possibilità sia di overflow un numero che di wraparound un numero quando si tenta di memorizzare un valore più grande del tipo di dati è stato progettato per memorizzare, a seconda che il valore sia calcolato al momento della compilazione o al momento dell’esecuzione. Un errore di compilazione si verifica quando il programma trova un errore cercando di costruire il programma. Un errore di esecuzione si verifica dopo che il programma è stato compilato, mentre è effettivamente in esecuzione.

Nel seguente esempio, impostiamo maxUint32 al suo valore massimo:

package main

import "fmt"

func main() {
	var maxUint32 uint32 = 4294967295 // Max uint32 size
	fmt.Println(maxUint32)
}

Si compilerà e si eseguirà con il seguente risultato:

Output
4294967295

Se aggiungiamo 1 al valore al momento dell’esecuzione, wrapperà a 0:

Output
0

D’altra parte, modificiamo il programma per aggiungere 1 alla variabile quando la assegnamo, prima della compilazione:

package main

import "fmt"

func main() {
	var maxUint32 uint32 = 4294967295 + 1
	fmt.Println(maxUint32)

}

Al momento della compilazione, se il compilatore può determinare che un valore sarà troppo grande per essere contenuto nel tipo di dati specificato, lancerà un errore overflow. Questo significa che il valore calcolato è troppo grande per il tipo di dati specificato.

Dato che il compilatore può determinare che il valore scarterà, ora lancerà un errore:

Output
prog.go:6:36: constant 4294967296 overflows uint32

Capire i confini dei tuoi dati ti aiuterà a evitare potenziali bug nel tuo programma in futuro.

Ora che abbiamo trattato i tipi numerici, vediamo come memorizzare i valori booleani.

Booleani

Il tipo di dato booleano può assumere uno di due valori, o true o false, e viene definito come bool quando lo si dichiara come tipo di dato. I booleani sono utilizzati per rappresentare i valori di verità associati alla branca logica della matematica, che informa gli algoritmi nell’informatica.

I valori true e false saranno sempre con una lettera minuscola t e f rispettivamente, poiché sono identificatori pre-dichiarati in Go.

Molte operazioni in matematica ci danno risposte che valutano o vero o falso:

  • maggiore di
    • 500 > 100 true
    • 1 > 5 false
  • minore di
    • 200 < 400 true
    • 4 < 2 false
  • uguale
    • 5 = 5 true
    • 500 = 400 false

Come con i numeri, possiamo memorizzare un valore booleano in una variabile:

myBool := 5 > 8

Possiamo quindi stampare il valore booleano con una chiamata alla funzione fmt.Println():

fmt.Println(myBool)

Poiché 5 non è maggiore di 8, riceveremo il seguente output:

Output
false

Mentre scrivi più programmi in Go, diventerai più familiare con il funzionamento dei booleani e con come diverse funzioni e operazioni che valutano a true o false possano cambiare il corso del programma.

Stringhe

Una stringa è una sequenza di uno o più caratteri (lettere, numeri, simboli) che può essere sia una costante che una variabile. Le stringhe esistono all’interno di apici inversi ` o doppi apici " in Go e hanno diverse caratteristiche a seconda degli apici utilizzati.

Se utilizzi gli apici inversi, stai creando un letterale di stringa grezzo. Se utilizzi i doppi apici, stai creando un letterale di stringa interpretato.

Letterali di Stringa Grezzi

I letterali di stringa grezzi sono sequenze di caratteri tra apici inversi, spesso chiamati back ticks. All’interno degli apici, qualsiasi carattere apparirà esattamente come è visualizzato tra gli apici inversi, ad eccezione del carattere di apice inverso stesso.

a := `Say "hello" to Go!`
fmt.Println(a)
Output
Say "hello" to Go!

Di solito, le barre rovesciate vengono utilizzate per rappresentare caratteri speciali nelle stringhe. Ad esempio, in una stringa interpretata, \n rappresenterebbe una nuova riga in una stringa. Tuttavia, le barre rovesciate non hanno un significato speciale all’interno di letterali di stringa grezza:

a := `Say "hello" to Go!\n`
fmt.Println(a)

Poiché la barra rovesciata non ha un significato speciale in un letterale di stringa, stamperà effettivamente il valore di \n invece di creare una nuova riga:

Output
Say "hello" to Go!\n

I letterali di stringa grezza possono anche essere utilizzati per creare stringhe su più righe:

a := `This string is on 
multiple lines
within a single back 
quote on either side.`
fmt.Println(a)
Output
This string is on multiple lines within a single back quote on either side.

Nei blocchi di codice precedenti, le nuove righe sono state trasferite letteralmente dall’input all’output.

Letterali di Stringa Interpretati

I letterali di stringa interpretati sono sequenze di caratteri tra virgolette doppie, come in "bar". All’interno delle virgolette, qualsiasi carattere può apparire ad eccezione della nuova riga e delle virgolette doppie non scappate. Per mostrare le virgolette doppie in una stringa interpretata, puoi utilizzare la barra rovesciata come carattere di escape, così:

a := "Say \"hello\" to Go!"
fmt.Println(a)
Output
Say "hello" to Go!

Quasi sempre utilizzerai letterali di stringa interpretati perché consentono l’uso di caratteri di escape al loro interno. Per ulteriori informazioni sull’uso delle stringhe, dai un’occhiata a Un’Introduzione all’Uso delle Stringhe in Go.

Stringhe con Caratteri UTF-8

UTF-8 è uno schema di codifica utilizzato per codificare caratteri a larghezza variabile in uno a quattro byte. Go supporta i caratteri UTF-8 fin dall’inizio, senza alcuna configurazione speciale, librerie o pacchetti. I caratteri romani come la lettera A possono essere rappresentati da un valore ASCII come il numero 65. Tuttavia, con caratteri speciali come un carattere internazionale di , è richiesto UTF-8. Go utilizza il tipo alias rune per i dati UTF-8.

a := "Hello, 世界"

Puoi utilizzare la parola chiave range in un ciclo for per indicizzare attraverso qualsiasi stringa in Go, anche una stringa UTF-8. I cicli for e range saranno trattati più in profondità più avanti nella serie; per ora, è importante sapere che possiamo usarlo per contare i byte in una determinata stringa:

package main

import "fmt"

func main() {
	a := "Hello, 世界"
	for i, c := range a {
		fmt.Printf("%d: %s\n", i, string(c))
	}
	fmt.Println("length of 'Hello, 世界': ", len(a))
}

Nel blocco di codice sopra, abbiamo dichiarato la variabile a e le abbiamo assegnato il valore di Hello, 世界. Il testo assegnato contiene caratteri UTF-8.

Abbiamo quindi utilizzato un ciclo for standard e la parola chiave range. In Go, la parola chiave range indicizzerà attraverso una stringa restituendo un carattere alla volta, nonché l’indice del byte in cui si trova il carattere nella stringa.

Utilizzando la funzione fmt.Printf, abbiamo fornito una stringa di formato %d: %s\n. %d è il verbo di stampa per una cifra (in questo caso un numero intero), e %s è il verbo di stampa per una stringa. Abbiamo quindi fornito i valori di i, che è l’indice corrente del ciclo for, e c, che è il carattere corrente nel ciclo for.

Infine, abbiamo stampato l’intera lunghezza della variabile a utilizzando la funzione integrata len.

In precedenza, abbiamo menzionato che un rune è un alias per int32 e può essere composto da uno a quattro byte. Il carattere richiede tre byte per essere definito e l’indice si sposta di conseguenza durante l’iterazione attraverso la stringa UTF-8. Questo è il motivo per cui i non è sequenziale quando viene stampato.

Output
0: H 1: e 2: l 3: l 4: o 5: , 6: 7: 世 10: 界 length of 'Hello, 世界': 13

Come puoi vedere, la lunghezza è maggiore del numero di volte in cui è stato necessario iterare sulla stringa.

Non lavorerai sempre con stringhe UTF-8, ma quando lo farai, ora capirai perché sono rune e non un singolo int32.

Dichiarazione dei Tipi di Dati per le Variabili

Ora che conosci i diversi tipi di dati primitivi, passeremo a come assegnare questi tipi alle variabili in Go.

In Go, possiamo definire una variabile con la parola chiave var seguita dal nome della variabile e dal tipo di dati desiderato.

Nel seguente esempio, dichiareremo una variabile chiamata pi di tipo float64.

La parola chiave var è la prima cosa dichiarata:

var pi float64

Seguita dal nome della nostra variabile, pi:

var pi float64

E infine il tipo di dato float64:

var pi float64

Possiamo anche specificare opzionalmente un valore iniziale, come 3.14:

var pi float64 = 3.14

Go è un linguaggio staticamente tipizzato. Staticamente tipizzato significa che ogni istruzione nel programma viene controllata in fase di compilazione. Significa anche che il tipo di dato è associato alla variabile, mentre nei linguaggi dinamicamente tipizzati, il tipo di dato è associato al valore.

Ad esempio, in Go, il tipo viene dichiarato al momento della dichiarazione di una variabile:

var pi float64 = 3.14
var week int = 7

Ognuna di queste variabili potrebbe essere di un tipo di dato diverso se le dichiari in modo diverso.

Questo è diverso da un linguaggio come PHP, dove il tipo di dato è associato al valore:

$s = "sammy";         // $s è automaticamente una stringa
$s = 123;             // $s è automaticamente un intero

Nel blocco di codice precedente, la prima $s è una stringa perché è assegnato il valore "sammy", e la seconda è un intero perché ha il valore 123.

Successivamente, diamo un’occhiata a tipi di dati più complessi come gli array.

Array

Un array è una sequenza ordinata di elementi. La capacità di un array è definita al momento della creazione. Una volta che un array ha allocato la sua dimensione, questa non può più essere modificata. Poiché la dimensione di un array è statica, significa che alloca la memoria una sola volta. Questo rende gli array un po’ rigidi da utilizzare, ma aumenta le prestazioni del programma. Per questo motivo, gli array sono tipicamente utilizzati quando si ottimizzano i programmi. Slice, che tratteremo successivamente, sono più flessibili e rappresentano ciò che si pensa come array in altri linguaggi.

Gli array sono definiti dichiarando la dimensione dell’array, seguita dal tipo di dati con i valori definiti tra parentesi graffe { }.

Un array di stringhe ha questo aspetto:

[3]string{"blue coral", "staghorn coral", "pillar coral"}

Possiamo memorizzare un array in una variabile e stamparlo:

coral := [3]string{"blue coral", "staghorn coral", "pillar coral"}
fmt.Println(coral)
Output
[blue coral staghorn coral pillar coral]

Come accennato in precedenza, le slice sono simili agli array, ma sono molto più flessibili. Diamo un’occhiata a questo tipo di dati mutabile.

Slice

Una slice è una sequenza ordinata di elementi che può cambiare di lunghezza. Le slice possono aumentare la loro dimensione dinamicamente. Quando aggiungi nuovi elementi a una slice, se questa non ha abbastanza memoria per memorizzare i nuovi elementi, richiederà più memoria dal sistema secondo necessità. Poiché una slice può essere espansa per aggiungere più elementi quando necessario, sono più comunemente utilizzate rispetto agli array.

Gli slice sono definiti dichiarando il tipo di dato preceduto da una parentesi quadra aperta e chiusa [] e avendo i valori tra parentesi graffe { }.

Uno slice di interi ha questo aspetto:

[]int{-3, -2, -1, 0, 1, 2, 3}

Uno slice di float ha questo aspetto:

[]float64{3.14, 9.23, 111.11, 312.12, 1.05}

Uno slice di stringhe ha questo aspetto:

[]string{"shark", "cuttlefish", "squid", "mantis shrimp"}

Definiamo il nostro slice di stringhe come seaCreatures:

seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp"}

Possiamo stamparli chiamando la variabile:

fmt.Println(seaCreatures)

L’output sarà esattamente come la lista che abbiamo creato:

Output
[shark cuttlefish squid mantis shrimp]

Possiamo usare la parola chiave append per aggiungere un elemento al nostro slice. Il seguente comando aggiungerà il valore stringa di seahorse allo slice:

seaCreatures = append(seaCreatures, "seahorse")

Puoi verificare che sia stato aggiunto stampandolo:

fmt.Println(seaCreatures)
Output
[shark cuttlefish squid mantis shrimp seahorse]

Come puoi vedere, se hai bisogno di gestire un numero sconosciuto di elementi, uno slice sarà molto più versatile di un array.

Mappe

La mappa è il tipo hash o dizionario integrato di Go. Le mappe utilizzano chiavi e valori come coppia per memorizzare i dati. Questo è utile nella programmazione per cercare rapidamente i valori tramite un indice, o in questo caso, una chiave. Ad esempio, potresti voler mantenere una mappa di utenti, indicizzata per il loro ID utente. La chiave sarebbe l’ID utente e l’oggetto utente sarebbe il valore. Una mappa è costruita utilizzando la parola chiave map seguita dal tipo di dato della chiave tra parentesi quadre [ ], seguito dal tipo di dato del valore e dalle coppie chiave-valore tra parentesi graffe.

map[key]value{}

Tipicamente utilizzati per contenere dati correlati, come le informazioni contenute in un ID, una mappa ha questo aspetto:

map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}

Noterete che oltre alle parentesi graffe, ci sono anche i due punti che attraversano la mappa. Le parole a sinistra dei due punti sono le chiavi. Le chiavi possono essere qualsiasi tipo comparabile in Go. I tipi comparabili sono tipi primitivi come stringhe, interi, ecc. Un tipo primitivo è definito dalla lingua e non è costruito combinando altri tipi. Sebbene possano essere tipi definiti dall’utente, è considerata una buona pratica mantenerli semplici per evitare errori di programmazione. Le chiavi nel dizionario sopra sono: nome, animale, colore e località.

Le parole a destra dei due punti sono i valori. I valori possono essere composti da qualsiasi tipo di dato. I valori nel dizionario sopra sono: Sammy, squalo, blu e oceano.

Memorizziamo la mappa all’interno di una variabile e stampiamola:

sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
fmt.Println(sammy)
Output
map[animal:shark color:blue location:ocean name:Sammy]

Se vogliamo isolare il colore di Sammy, possiamo farlo chiamando sammy["colore"]. Stampiamolo:

fmt.Println(sammy["color"])
Output
blue

Poiché le mappe offrono coppie chiave-valore per memorizzare dati, possono essere elementi importanti nel vostro programma Go.

Conclusione

Al questo punto, dovresti avere una comprensione migliore di alcuni dei principali tipi di dati disponibili per l’uso in Go. Ogniuno di questi tipi di dati diventerà importante man mano che sviluppi progetti di programmazione nella lingua Go.

Una volta che hai una buona padronanza dei tipi di dati disponibili in Go, puoi imparare Come Convertire Tipi di Dati per cambiare i tuoi tipi di dati secondo la situazione.

Source:
https://www.digitalocean.com/community/tutorials/understanding-data-types-in-go