Go - from Google



Le stringhe       

E' una struttura dati potente e molto importante in questo linguaggio (come è importante in quasi tutti gli altri che conosco, peraltro). Ne abbiamo già accennato nel capitolo 2, ricordate? Le stringhe sono sostanzialmente sequenze di caratteri. Che siano costituite da caratteri Unicode o solo un sottoinsieme poco importa a livello di definizione: sempre di sequenze di caratteri si tratta. Go gestisce certamente la codifica Unicode che è stata standardizzata per permettere l'uso del formato testo anche con lingue diverse da quelle anglosassoni / europee, tant'è che a tutto'oggi vi sono un gran numero di caratteri definiti, nello standard, oltre 100.000, se ben ricordo ma se date uno sguardo su Wikipedia o sul sito ufficiale troverete tutti i dettagli. Come in altri linguaggi le stringhe sono immutabili ovvero non possono essere modificate in loco. Tuttavia, andando un po' più in fondo, qualche differenza esiste in particolare le stringhe sono sequenza di caratteri a lunghezza variabile invece che rigidamente fissa come in molti altri linguaggi (Java, che usa sempre 2 bytes, o Python, per dirne un paio). Quindi ogni carattere può essere rappresentato da uno o più bytes a seconda delle necessità. Secondo me è un piccolo passo avanti rivolto alla minimizzazione di occupazione di spazio in memoria.
In Go le stringhe sono rappresentate in due modi: racchiuse in mezzo ad una coppia di doppia pici e racchiuse in mezzo ad una coppia di singoli apici. Ad esempio:

"ciao"
'ciao'


sono esempi validi e quasi equivalenti per il linguaggio. Quel "quasi" nasconde in realtà una profonda differenza. Le stringhe all'interno dei doppi apici sono interpretate quelle comprese all'interno dei singoli apici sono stringhe "raw". Le stringhe interpretate possono contenere al loro interno le famose sequenze di escape note in tutti i linguaggi le quali, appunto, sono oggetto di interpretazione. Le stringhe raw invece hanno la possibilità di essere spezzate su più righe. Presentiamo la consueta tabella delle sequenze di escape

Sequenza Significato
\n a capo
\t tabulazione orizzontale
\v tabulazione verticale
\b backspace, un carattere indietro
\r ritorno del carrello
\a beep di allarme
\\ backslash
\? ?
\' singolo apice '
\" doppio apice "

Queste sequenze in una raw string non ha effetto. Vediamo l'esempio:

  esempio 6.1
1
2
3
4
5
6
7
8
9
10
11
12
package main
import "fmt"

func main() {
  s1 := "12\n34"
  fmt.Println(s1)
  s2 := `12\n34`
  fmt.Println(s2)
  s3 := `12
  34`
  fmt.Println(s3)
}

L'output ci fa capire le differenze:

12
34
12\n34
12
34


viene comunque ribadito quanto appena espresso. Da notare che l'apice singolo corrisponde al carattere ASCII 96. Attenzione che le tastiere italiane propongono il carattere 39 come apice standard. Non va bene.
In quanto elementi sequenziali i singoli componenti di una stringa sono individuabili tramite l'operatore [] che estrare il valore "raw" in una specifica locazione. Il primo elemento ha indice 0 l'ultimo, dal momento che si procede singolarmente, è pari al numero di elementi della stringa meno uno, come è facile intuire.
Le stringhe sono correlate ad un gran numero di operazioni che possiamo compiere su di esse. Di seguito ne elenchiamo alcune:

  esempio 6.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import "fmt"

func main() {
  s1 := ""
  s1 = s1 + "abcd"
  s1 += "efgh"
  fmt.Println(s1[2:5])
  fmt.Println(s1[2:])
  fmt.Println(s1[2])
  fmt.Println(s1[2:3])
  fmt.Println(s1[7:8])
  fmt.Println(len(s1))
  fmt.Println(len([]rune(s1)))
  fmt.Println(s1)
}

Riga 5: viene definita una stringa vuota
Riga 6: l'operatore + concatena due stringhe
Riga 7: stesso effetto della precedente, l'operazione è quella di "append"
Riga 8: operando con [x : y] sdi tampa una sottostringa che va da x a y-1. Quindi l'istruzione in oggetto stampa la sequenza dei caratteri [2][3][4]
Riga 9: stampa dal carattere all'indice 2 fino alla fine della stringa
Riga 10: stampa il carattere contenuto all'indice 2 in mod "raw", ovvero stampa il numero ASCII che lo rappresenta.
Riga 11: trick per stampare il carattere in posizione 2 come stringa (stampa la lettera c)
Riga 12: stampa l'ultimo carattere
Riga 13: restituisce la lunghezza in byte della stringa
Riga 14: restituisce il numero di elementi della stringa
Riga 15: ovviamente stampa la stringa

E' possibile usare i normali operatori per confrontare due stringhe; sono 4

<  ==  !=   >

Tutti questi operatori lavorano confrontando byte dopo byte in memoria. Di seguito un rapido esempio:

  esempio 6.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"

func main() {
  s1 := "aabbcc"
  s2 := "abc"
  s3 := "aaaaaaa"
  s4 := "aabbcc"
  fmt.Println(s1 > s2)
  fmt.Println(s1 == s4)
  fmt.Println(s1 > s3)
  fmt.Println(s3 > s4)
fmt.Println(s3 != s4)
}

L'output è:

false
true
true
false
true


Non è difficile capire perchè: Il primo confronto diventa false quando la seconda lettera di s1 viene confrontata con la seconda di s2: a è < di b. Il secondo confronto è vero perchè confronta stringhe di lunghezza uguale composte dalle stesse lettere nello stesso ordine. Il terzo confronto è true perchè è vero che s3 è più lunga di s1 ma questo non conta o meglio è meno importante, rispetto al fatto che il terzo carattere di s3, ovvero una a è minore del terzo di s1, che è una b. Per lo stesso motivo è falso, come ovvio, che s3 possa essere > di s4. E' infine vero che s3 è diverso da s4. Potete fare altri esperimenti a piacimento. Peraltro questi confronti sono ricchi di insidie quando si prendono in esame caratteri particolari. In rete troverete parecchi esempi che testimoniano questo fatto in quanto si tratta di un problema comune a tutti i linguaggi che usano lo standard Unicode.

Nel capitolo 2 abbiamo presentato il package string che contiene molte utili funzioni applicabili alle nostre stinghe. Un altro utile package ovvero strconv che permette la conversione di una stringa in molti altri formati. L'elenco è fornito qui di seguito, nel mentre darò un esempio di utilizzo della comoda funzione Atoi che converte da stringa ad intero:

  esempio 6.4
1
2
3
4
5
6
7
8
9
package main
import "fmt"
import "strconv"

func main() {
s1 := "1000"
x, _ := strconv.Atoi(s1)
fmt.Println(x + 1)
}

Da notare, alla riga 9, il doppio assegnamento, dovuto al fatto che Atoi emette due parametri, uno riferito al valore convertito, l'altro ad un eventuale codice di errore. La sua definizione interna infatti è:

func Atoi(s string) (i int, err error)

Le funzioni presenti in strconv, che eventualmente approndiremo in seguito e la cui definizione formale è facilmente reperibile sul sito ufficiale, sono:

func AppendBool(dst []byte, b bool) []byte
func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int) []byte
func AppendInt(dst []byte, i int64, base int) []byte
func AppendQuote(dst []byte, s string) []byte
func AppendQuoteRune(dst []byte, r rune) []byte
func AppendQuoteRuneToASCII(dst []byte, r rune) []byte
func AppendQuoteToASCII(dst []byte, s string) []byte
func AppendUint(dst []byte, i uint64, base int) []byte
func Atoi(s string) (i int, err error)
func CanBackquote(s string) bool
func FormatBool(b bool) string
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
func FormatInt(i int64, base int) string
func FormatUint(i uint64, base int) string
func IsPrint(r rune) bool
func Itoa(i int) string
func ParseBool(str string) (value bool, err error)
func ParseFloat(s string, bitSize int) (f float64, err error)
func ParseInt(s string, base int, bitSize int) (i int64, err error)
func ParseUint(s string, base int, bitSize int) (n uint64, err error)
func Quote(s string) string
func QuoteRune(r rune) string
func QuoteRuneToASCII(r rune) string
func QuoteToASCII(s string) string
func Unquote(s string) (t string, err error)
func UnquoteChar(s string, quote byte) (value rune, multibyte bool, tail string, err error)